diff --git a/Cargo.lock b/Cargo.lock index 758f77ad6ff..6e50f42f409 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,20 +56,22 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -227,6 +229,15 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.3" @@ -242,6 +253,16 @@ dependencies = [ "critical-section", ] +[[package]] +name = "atomic-write-file" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c232177ba50b16fe7a4588495bd474a62a9e45a8e4ca6fd7d0b7ac29d164631e" +dependencies = [ + "nix 0.26.4", + "rand", +] + [[package]] name = "atsamd-hal" version = "0.16.0" @@ -358,7 +379,7 @@ dependencies = [ "hex", "http", "hyper", - "ring 0.17.5", + "ring", "time", "tokio", "tracing", @@ -712,9 +733,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64-simd" @@ -732,7 +753,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c5b0a88aa36e9f095ee2e2b13fb8c5e4313e022783aedacc123328c0084916d" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", ] [[package]] @@ -743,9 +764,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "basic-toml" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bfc506e7a2370ec239e1d072507b2a80c833083699d3c6fa176fbb4de8448c6" +checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" dependencies = [ "serde", ] @@ -794,9 +815,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" dependencies = [ "serde", ] @@ -847,7 +868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce7d4413c940e8e3cb6afc122d3f4a07096aca259d286781128683fc9f39d9b" dependencies = [ "async-trait", - "bitflags 2.4.0", + "bitflags 2.4.1", "bluez-generated", "dbus", "dbus-tokio", @@ -881,23 +902,23 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" dependencies = [ "memchr", - "regex-automata 0.3.9", + "regex-automata 0.4.3", "serde", ] [[package]] name = "btleplug" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba674f1e8564205eeb1406abca78f3f4beff2408ad6b06a970da23fadeba2534" +checksum = "0743700186f3d4c46f91b5ac9534e0c271a5de1c3ebd7dca1b5b4216132f72a0" dependencies = [ "async-trait", - "bitflags 2.4.0", + "bitflags 2.4.1", "bluez-async", "cocoa", "dashmap", @@ -914,7 +935,7 @@ dependencies = [ "tokio", "tokio-stream", "uuid", - "windows 0.51.1", + "windows", ] [[package]] @@ -956,9 +977,9 @@ dependencies = [ [[package]] name = "bytes-utils" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" dependencies = [ "bytes 1.5.0", "either", @@ -1045,9 +1066,9 @@ dependencies = [ [[package]] name = "chunked_transfer" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] name = "cipher" @@ -1086,9 +1107,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.7" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" dependencies = [ "clap_builder", "clap_derive", @@ -1096,9 +1117,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.7" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" dependencies = [ "anstream", "anstyle", @@ -1113,7 +1134,7 @@ version = "4.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bffe91f06a11b4b9420f62103854e90867812cd5d01557f853c5ee8e791b12ae" dependencies = [ - "clap 4.4.7", + "clap 4.4.8", ] [[package]] @@ -1149,7 +1170,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3be86020147691e1d2ef58f75346a3d4d94807bfc473e377d52f09f0f7d77f7" dependencies = [ - "clap 4.4.7", + "clap 4.4.8", "roff", ] @@ -1386,9 +1407,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -1404,9 +1425,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32c" @@ -1491,7 +1512,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "crossterm_winapi", "futures-core", "libc", @@ -1514,9 +1535,9 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array 0.14.7", "rand_core", @@ -1593,9 +1614,9 @@ dependencies = [ [[package]] name = "curve25519-dalek-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", @@ -1693,9 +1714,9 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "data-encoding-macro" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c904b33cc60130e1aeea4956ab803d08a3f4a0ca82d64ed757afac3891f2bb99" +checksum = "20c01c06f5f429efdf2bae21eb67c28b3df3cf85b7dd2d8ef09c0838dac5d33e" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1703,9 +1724,9 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fdf3fce3ce863539ec1d7fd1b6dcc3c645663376b43ed376bbf887733e4f772" +checksum = "0047d07f2c89b17dd631c80450d69841a6b5d7fb17278cbc43d7e4cfcf2576f3" dependencies = [ "data-encoding", "syn 1.0.109", @@ -1863,6 +1884,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast" version = "0.11.0" @@ -1901,9 +1928,9 @@ checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ecdsa" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", "digest", @@ -1915,9 +1942,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "signature", @@ -1925,15 +1952,16 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" dependencies = [ "curve25519-dalek", "ed25519", "rand_core", "serde", "sha2", + "subtle", "zeroize", ] @@ -1942,12 +1970,15 @@ name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -2069,25 +2100,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ - "errno-dragonfly", "libc", "windows-sys 0.48.0", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "error-code" version = "2.3.1" @@ -2107,6 +2127,23 @@ dependencies = [ "rustversion", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "example_blocks" version = "0.1.0" @@ -2128,9 +2165,9 @@ dependencies = [ [[package]] name = "fake" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92ee0b58c77663520eb3b054af6f83ca98722fed454d19b9c712b06329e2019" +checksum = "26221445034074d46b276e13eb97a265ebdb8ed8da705c4dddd3dd20b66b45d2" dependencies = [ "deunicode", "dummy", @@ -2169,9 +2206,9 @@ dependencies = [ [[package]] name = "fdeflate" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" dependencies = [ "simd-adler32", ] @@ -2188,9 +2225,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.1" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "file_diff" @@ -2213,6 +2250,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "flate2" version = "1.0.28" @@ -2238,6 +2281,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2288,9 +2342,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -2378,6 +2432,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.29" @@ -2493,9 +2558,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -2514,9 +2579,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -2537,9 +2602,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes 1.5.0", "fnv", @@ -2547,7 +2612,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -2622,6 +2687,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hello_ockam" @@ -2702,9 +2770,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes 1.5.0", "fnv", @@ -2751,7 +2819,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -2760,9 +2828,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", @@ -2776,16 +2844,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows 0.48.0", + "windows-core 0.51.1", ] [[package]] @@ -2805,9 +2873,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2899,9 +2967,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-docker" @@ -3006,9 +3074,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -3037,6 +3105,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "leb128" @@ -3061,9 +3132,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libp2p-identity" @@ -3087,6 +3158,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -3108,38 +3180,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" - -[[package]] -name = "lmdb-rkv" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447a296f7aca299cfbb50f4e4f3d49451549af655fb7215d7f8c0c3d64bad42b" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "libc", - "lmdb-rkv-sys", -] - -[[package]] -name = "lmdb-rkv-sys" -version = "0.11.2" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b9ce6b3be08acefa3003c57b7565377432a89ec24476bbe72e11d101f852fe" -dependencies = [ - "cc", - "libc", - "pkg-config", -] +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -3448,6 +3497,7 @@ dependencies = [ "cfg-if", "libc", "memoffset 0.7.1", + "pin-utils", ] [[package]] @@ -3456,7 +3506,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "libc", ] @@ -3505,6 +3555,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -3515,6 +3582,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -3528,9 +3606,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -3628,11 +3706,11 @@ name = "ockam_abac" version = "0.39.0" dependencies = [ "either", - "lmdb-rkv", "minicbor", "ockam_core", "ockam_executor", "ockam_identity", + "ockam_node", "once_cell", "quickcheck", "rand", @@ -3640,6 +3718,7 @@ dependencies = [ "rusqlite", "rustyline", "rustyline-derive", + "sqlx", "str-buf 3.0.2", "tempfile", "tokio", @@ -3659,6 +3738,7 @@ dependencies = [ "either", "fake", "fs2", + "futures 0.3.29", "hex", "home", "indexmap 2.1.0", @@ -3684,6 +3764,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sqlx", "sysinfo", "tempfile", "thiserror", @@ -3714,6 +3795,8 @@ dependencies = [ "ockam_transport_tcp", "serde", "serde_json", + "sqlx", + "tempfile", "thiserror", "tokio", "tokio-retry", @@ -3731,7 +3814,7 @@ dependencies = [ "arboard", "assert_cmd", "async-trait", - "clap 4.4.7", + "clap 4.4.8", "clap_complete", "clap_mangen", "cli-table", @@ -3742,6 +3825,7 @@ dependencies = [ "dialoguer", "duct", "flate2", + "futures 0.3.29", "hex", "home", "indicatif", @@ -3843,11 +3927,11 @@ version = "0.93.0" dependencies = [ "async-trait", "cfg-if", + "chrono", "delegate", "group", "heapless", "hex", - "lmdb-rkv", "minicbor", "ockam_core", "ockam_macros", @@ -3859,12 +3943,12 @@ dependencies = [ "quickcheck_macros", "rand", "rand_xorshift", - "rusqlite", "serde", "serde-big-array", "serde_bare", "serde_json", "sha2", + "sqlx", "subtle", "tempfile", "time", @@ -3927,8 +4011,11 @@ dependencies = [ "serde", "serde_bare", "serde_json", + "sqlx", "tempfile", + "time", "tokio", + "tokio-retry", "tracing", "tracing-error", "tracing-subscriber", @@ -4060,6 +4147,7 @@ dependencies = [ "serde_cbor", "serde_json", "sha2", + "sqlx", "static_assertions", "tempfile", "thiserror", @@ -4127,9 +4215,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfabf1927dce4d6fdf563d63328a0a506101ced3ec780ca2135747336c98cef8" +checksum = "90878fb664448b54c4e592455ad02831e23a3f7e157374a8b95654731aac7349" dependencies = [ "is-wsl", "libc", @@ -4154,9 +4242,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.5.1" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "outref" @@ -4200,9 +4288,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", @@ -4234,9 +4322,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petname" @@ -4307,6 +4395,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -4325,18 +4424,18 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "platforms" -version = "3.1.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" [[package]] name = "plist" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" +checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" dependencies = [ - "base64 0.21.4", - "indexmap 1.9.3", + "base64 0.21.5", + "indexmap 2.1.0", "line-wrap", "quick-xml", "serde", @@ -4370,9 +4469,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.4.3" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b" +checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" [[package]] name = "powerfmt" @@ -4440,9 +4539,9 @@ dependencies = [ [[package]] name = "primeorder" -version = "0.13.2" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ "elliptic-curve", ] @@ -4499,7 +4598,7 @@ checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.4.0", + "bitflags 2.4.1", "lazy_static", "num-traits", "rand", @@ -4528,9 +4627,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] @@ -4645,7 +4744,7 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2770cf28e5764c238234ff01315f6e5a3185e9d4950b342f8f4ac9a778bebd2" dependencies = [ - "clap 4.4.7", + "clap 4.4.8", "crossterm", "is-terminal", "log", @@ -4734,9 +4833,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] @@ -4782,12 +4881,6 @@ dependencies = [ "regex-syntax 0.6.29", ] -[[package]] -name = "regex-automata" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" - [[package]] name = "regex-automata" version = "0.4.3" @@ -4823,7 +4916,7 @@ version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "bytes 1.5.0", "encoding_rs", "futures-core", @@ -4867,21 +4960,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.5" @@ -4892,7 +4970,7 @@ dependencies = [ "getrandom", "libc", "spin 0.9.8", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.48.0", ] @@ -4913,13 +4991,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +[[package]] +name = "rsa" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3211b01eea83d80687da9eef70e39d65144a3894866a5153a2723e425a157f" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rusqlite" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4948,16 +5046,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.19", + "semver 1.0.20", ] [[package]] name = "rustix" -version = "0.38.17" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -4966,12 +5064,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", - "ring 0.17.5", + "ring", "rustls-webpki", "sct", ] @@ -4990,11 +5088,11 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", ] [[package]] @@ -5003,8 +5101,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.5", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -5031,7 +5129,7 @@ version = "12.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "clipboard-win", "fd-lock", @@ -5097,12 +5195,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring", + "untrusted", ] [[package]] @@ -5153,9 +5251,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "semver-parser" @@ -5385,9 +5483,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core", @@ -5410,9 +5508,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "smawk" @@ -5428,9 +5526,9 @@ checksum = "5e9f0ab6ef7eb7353d9119c170a436d1bf248eea575ac42d19d12f4e34130831" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -5471,6 +5569,213 @@ dependencies = [ "der", ] +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools 0.11.0", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes 1.5.0", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.1.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +dependencies = [ + "atomic-write-file", + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +dependencies = [ + "atoi", + "base64 0.21.5", + "bitflags 2.4.1", + "byteorder", + "bytes 1.5.0", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +dependencies = [ + "atoi", + "base64 0.21.5", + "bitflags 2.4.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -5511,7 +5816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c35360daaceb356866a3f840e845dd09c1ef69aed52637520b4c5c409c10cdc0" dependencies = [ "bare-metal 1.0.0", - "bitflags 2.4.0", + "bitflags 2.4.1", "cortex-m 0.7.7", "cortex-m-rt", "embedded-dma", @@ -5580,6 +5885,17 @@ dependencies = [ "bytes 1.5.0", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strip-ansi-escapes" version = "0.2.0" @@ -5775,9 +6091,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", @@ -5788,9 +6104,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -6034,9 +6350,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes 1.5.0", "futures-core", @@ -6067,6 +6383,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -6074,11 +6391,12 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", + "thiserror", "time", "tracing-subscriber", ] @@ -6248,6 +6566,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.4.0" @@ -6276,12 +6600,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -6290,9 +6608,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -6361,9 +6679,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "volatile-register" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee8f19f9d74293faf70901bc20ad067dc1ad390d2cbf1e3f75f721ffee908b6" +checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" dependencies = [ "vcell", ] @@ -6430,9 +6748,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -6440,9 +6758,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", @@ -6455,9 +6773,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" dependencies = [ "cfg-if", "js-sys", @@ -6467,9 +6785,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6477,9 +6795,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", @@ -6490,9 +6808,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "wasm-encoder" @@ -6517,9 +6835,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", @@ -6544,6 +6862,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "winapi" version = "0.3.9" @@ -6586,30 +6910,30 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-targets 0.48.5", + "windows-core 0.52.0", + "windows-targets 0.52.0", ] [[package]] -name = "windows" +name = "windows-core" version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-core", "windows-targets 0.48.5", ] [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -6660,6 +6984,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6672,6 +7011,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6684,6 +7029,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -6696,6 +7047,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -6708,6 +7065,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -6720,6 +7083,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -6732,6 +7101,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -6744,6 +7119,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winreg" version = "0.50.0" @@ -6815,6 +7196,26 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "zerocopy" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/examples/rust/example_projects/no_std/Cargo.toml b/examples/rust/example_projects/no_std/Cargo.toml index f7821dadfe6..fa7cd24fa6e 100644 --- a/examples/rust/example_projects/no_std/Cargo.toml +++ b/examples/rust/example_projects/no_std/Cargo.toml @@ -52,7 +52,7 @@ log-semihosting = [] log-uart = [] [dependencies] -ockam = { path = "../../../../implementations/rust/ockam/ockam", default_features = false, features = ["software_vault"] } +ockam = { path = "../../../../implementations/rust/ockam/ockam", optional = true, default_features = false, features = ["software_vault"] } alloc-cortex-m = { version = "0.4.1", optional = true } cortex-m = { version = "0.7.2", optional = true } diff --git a/examples/rust/example_projects/no_std/examples/01-node.rs b/examples/rust/example_projects/no_std/examples/01-node.rs index 90cddf06e7c..c7661892a67 100644 --- a/examples/rust/example_projects/no_std/examples/01-node.rs +++ b/examples/rust/example_projects/no_std/examples/01-node.rs @@ -59,7 +59,7 @@ use ockam::{node, Context, Result}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { - let mut node = node(ctx); + let mut node = node(ctx).await?; // Stop the node as soon as it starts. info!("Stop the node as soon as it starts."); diff --git a/examples/rust/example_projects/no_std/examples/hello.rs b/examples/rust/example_projects/no_std/examples/hello.rs index f9002643f56..60f69ceeeee 100644 --- a/examples/rust/example_projects/no_std/examples/hello.rs +++ b/examples/rust/example_projects/no_std/examples/hello.rs @@ -67,7 +67,7 @@ use ockam::{node, route, Context, Result}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { - let mut node = node(ctx); + let mut node = node(ctx).await?; let bob = node.create_identity().await?; // Create a secure channel listener for Bob that will wait for requests to diff --git a/examples/rust/file_transfer/examples/receiver.rs b/examples/rust/file_transfer/examples/receiver.rs index d2089275f90..780c964e7d8 100644 --- a/examples/rust/file_transfer/examples/receiver.rs +++ b/examples/rust/file_transfer/examples/receiver.rs @@ -79,7 +79,7 @@ impl Worker for FileReception { #[ockam::node] async fn main(ctx: Context) -> Result<()> { - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Create an Identity to represent Receiver. diff --git a/examples/rust/file_transfer/examples/sender.rs b/examples/rust/file_transfer/examples/sender.rs index efa3e45745f..9b2bb8f4787 100644 --- a/examples/rust/file_transfer/examples/sender.rs +++ b/examples/rust/file_transfer/examples/sender.rs @@ -33,7 +33,7 @@ struct Opt { async fn main(ctx: Context) -> Result<()> { let opt = Opt::from_args(); - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Create an Identity to represent Sender. diff --git a/examples/rust/get_started/Cargo.toml b/examples/rust/get_started/Cargo.toml index 874036917c0..8796a55ca69 100644 --- a/examples/rust/get_started/Cargo.toml +++ b/examples/rust/get_started/Cargo.toml @@ -18,6 +18,7 @@ std = [ "serde_json/default", "ockam_multiaddr/std", "ockam_api/std", + "storage", ] # Feature: "no_std" enables functionality required for platforms @@ -27,6 +28,7 @@ no_std = ["ockam/no_std"] # Feature: "alloc" enables support for heap allocation on "no_std" # platforms, requires nightly. alloc = ["ockam/alloc", "serde_json/alloc"] +storage = ["ockam_api/storage"] [dependencies] anyhow = "1" diff --git a/examples/rust/get_started/examples/01-node.rs b/examples/rust/get_started/examples/01-node.rs index dd4d33c9369..7cea9c66f21 100644 --- a/examples/rust/get_started/examples/01-node.rs +++ b/examples/rust/get_started/examples/01-node.rs @@ -27,7 +27,7 @@ async fn main(ctx: Context) -> Result<()> { print_title(vec!["Run a node & stop it right away"]); // Create a node. - let mut node = node(ctx); + let mut node = node(ctx).await?; // Stop the node as soon as it starts. node.stop().await diff --git a/examples/rust/get_started/examples/02-worker.rs b/examples/rust/get_started/examples/02-worker.rs index a62e852a88e..550bfd7f6ff 100644 --- a/examples/rust/get_started/examples/02-worker.rs +++ b/examples/rust/get_started/examples/02-worker.rs @@ -6,7 +6,7 @@ use ockam::{node, Context, Result}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Start a worker, of type Echoer, at address "echoer" node.start_worker("echoer", Echoer).await?; diff --git a/examples/rust/get_started/examples/03-routing-many-hops.rs b/examples/rust/get_started/examples/03-routing-many-hops.rs index b9b692a8fd6..576c42e4250 100644 --- a/examples/rust/get_started/examples/03-routing-many-hops.rs +++ b/examples/rust/get_started/examples/03-routing-many-hops.rs @@ -6,7 +6,7 @@ use ockam::{node, route, Context, Result}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Start an Echoer worker at address "echoer" node.start_worker("echoer", Echoer).await?; diff --git a/examples/rust/get_started/examples/03-routing.rs b/examples/rust/get_started/examples/03-routing.rs index 3ee2c2d5e2d..5fcec17c8b2 100644 --- a/examples/rust/get_started/examples/03-routing.rs +++ b/examples/rust/get_started/examples/03-routing.rs @@ -6,7 +6,7 @@ use ockam::{node, route, Context, Result}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Start a worker, of type Echoer, at address "echoer" node.start_worker("echoer", Echoer).await?; diff --git a/examples/rust/get_started/examples/04-routing-over-transport-initiator.rs b/examples/rust/get_started/examples/04-routing-over-transport-initiator.rs index 2d0b9f24fd0..e94d0652656 100644 --- a/examples/rust/get_started/examples/04-routing-over-transport-initiator.rs +++ b/examples/rust/get_started/examples/04-routing-over-transport-initiator.rs @@ -5,7 +5,7 @@ use ockam::{node, route, Context, Result, TcpConnectionOptions, TcpTransportExte #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Initialize the TCP Transport. let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/04-routing-over-transport-responder.rs b/examples/rust/get_started/examples/04-routing-over-transport-responder.rs index 5d9566219e3..310585ea217 100644 --- a/examples/rust/get_started/examples/04-routing-over-transport-responder.rs +++ b/examples/rust/get_started/examples/04-routing-over-transport-responder.rs @@ -7,7 +7,7 @@ use ockam::{node, Context, Result, TcpListenerOptions, TcpTransportExtension}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/04-routing-over-transport-two-hops-initiator.rs b/examples/rust/get_started/examples/04-routing-over-transport-two-hops-initiator.rs index cee2e2b4dd7..23f9aa72ba6 100644 --- a/examples/rust/get_started/examples/04-routing-over-transport-two-hops-initiator.rs +++ b/examples/rust/get_started/examples/04-routing-over-transport-two-hops-initiator.rs @@ -5,7 +5,7 @@ use ockam::{node, route, Context, Result, TcpConnectionOptions, TcpTransportExte #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/04-routing-over-transport-two-hops-middle.rs b/examples/rust/get_started/examples/04-routing-over-transport-two-hops-middle.rs index a2032990764..bd77359850b 100644 --- a/examples/rust/get_started/examples/04-routing-over-transport-two-hops-middle.rs +++ b/examples/rust/get_started/examples/04-routing-over-transport-two-hops-middle.rs @@ -9,7 +9,7 @@ use ockam::{node, Context, Result, TcpConnectionOptions, TcpListenerOptions, Tcp #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/04-routing-over-transport-two-hops-responder.rs b/examples/rust/get_started/examples/04-routing-over-transport-two-hops-responder.rs index 5d9566219e3..310585ea217 100644 --- a/examples/rust/get_started/examples/04-routing-over-transport-two-hops-responder.rs +++ b/examples/rust/get_started/examples/04-routing-over-transport-two-hops-responder.rs @@ -7,7 +7,7 @@ use ockam::{node, Context, Result, TcpListenerOptions, TcpTransportExtension}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/04-udp-transport-initiator.rs b/examples/rust/get_started/examples/04-udp-transport-initiator.rs index f33f56cefa5..0d14e408f6b 100644 --- a/examples/rust/get_started/examples/04-udp-transport-initiator.rs +++ b/examples/rust/get_started/examples/04-udp-transport-initiator.rs @@ -6,7 +6,7 @@ use ockam_transport_udp::{UdpTransportExtension, UDP}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Initialize the UDP Transport let _udp = node.create_udp_transport().await?; diff --git a/examples/rust/get_started/examples/04-udp-transport-responder.rs b/examples/rust/get_started/examples/04-udp-transport-responder.rs index f1a3747bc92..be73a96c768 100644 --- a/examples/rust/get_started/examples/04-udp-transport-responder.rs +++ b/examples/rust/get_started/examples/04-udp-transport-responder.rs @@ -8,7 +8,7 @@ use ockam_transport_udp::UdpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the UDP Transport let udp = node.create_udp_transport().await?; diff --git a/examples/rust/get_started/examples/04-unix-domain-socket-transport-initiator.rs b/examples/rust/get_started/examples/04-unix-domain-socket-transport-initiator.rs index e6c1dc2f720..26ab1201c7d 100644 --- a/examples/rust/get_started/examples/04-unix-domain-socket-transport-initiator.rs +++ b/examples/rust/get_started/examples/04-unix-domain-socket-transport-initiator.rs @@ -6,7 +6,7 @@ use ockam_transport_uds::{UdsTransportExtension, UDS}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Initialize the UDS Transport let uds = node.create_uds_transport().await?; diff --git a/examples/rust/get_started/examples/04-unix-domain-socket-transport-responder.rs b/examples/rust/get_started/examples/04-unix-domain-socket-transport-responder.rs index 0b9860387a9..a654375e574 100644 --- a/examples/rust/get_started/examples/04-unix-domain-socket-transport-responder.rs +++ b/examples/rust/get_started/examples/04-unix-domain-socket-transport-responder.rs @@ -8,7 +8,7 @@ use ockam_transport_uds::UdsTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the UDS Transport let uds = node.create_uds_transport().await?; diff --git a/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-initiator.rs b/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-initiator.rs index 373350109d2..97f9c7eeb30 100644 --- a/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-initiator.rs +++ b/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-initiator.rs @@ -7,7 +7,7 @@ use ockam::{node, route, Context, Result, TcpConnectionOptions, TcpTransportExte #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Create an Identity to represent Alice. let alice = node.create_identity().await?; diff --git a/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-middle.rs b/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-middle.rs index 58375617131..4a16d8bebe5 100644 --- a/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-middle.rs +++ b/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-middle.rs @@ -9,7 +9,7 @@ use ockam::{node, Context, Result, TcpConnectionOptions, TcpListenerOptions, Tcp #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-responder.rs b/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-responder.rs index 1503c8d5c88..884ea115e4b 100644 --- a/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-responder.rs +++ b/examples/rust/get_started/examples/05-secure-channel-over-two-transport-hops-responder.rs @@ -8,7 +8,7 @@ use ockam::{node, Context, Result, TcpListenerOptions, TcpTransportExtension}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the TCP Transport. let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/06-credentials-exchange-client.rs b/examples/rust/get_started/examples/06-credentials-exchange-client.rs index eecfa5c7c4f..8157fec4aaa 100644 --- a/examples/rust/get_started/examples/06-credentials-exchange-client.rs +++ b/examples/rust/get_started/examples/06-credentials-exchange-client.rs @@ -9,7 +9,7 @@ use ockam_vault::{EdDSACurve25519SecretKey, SigningSecret, SoftwareVaultForSigni #[ockam::node] async fn main(ctx: Context) -> Result<()> { - let identity_vault = SoftwareVaultForSigning::create(); + let identity_vault = SoftwareVaultForSigning::create().await?; // Import the signing secret key to the Vault let secret = identity_vault .import_key(SigningSecret::EdDSACurve25519(EdDSACurve25519SecretKey::new( @@ -21,10 +21,10 @@ async fn main(ctx: Context) -> Result<()> { .await?; // Create a default Vault but use the signing vault with our secret in it - let mut vault = Vault::create(); + let mut vault = Vault::create().await?; vault.identity_vault = identity_vault; - let mut node = Node::builder().with_vault(vault).build(&ctx).await?; + let mut node = Node::builder().await?.with_vault(vault).build(&ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs b/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs index 932a5353738..80ba72f5771 100644 --- a/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs +++ b/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs @@ -9,7 +9,7 @@ use ockam_vault::{EdDSACurve25519SecretKey, SigningSecret, SoftwareVaultForSigni #[ockam::node] async fn main(ctx: Context) -> Result<()> { - let identity_vault = SoftwareVaultForSigning::create(); + let identity_vault = SoftwareVaultForSigning::create().await?; // Import the signing secret key to the Vault let secret = identity_vault .import_key(SigningSecret::EdDSACurve25519(EdDSACurve25519SecretKey::new( @@ -21,10 +21,10 @@ async fn main(ctx: Context) -> Result<()> { .await?; // Create a default Vault but use the signing vault with our secret in it - let mut vault = Vault::create(); + let mut vault = Vault::create().await?; vault.identity_vault = identity_vault; - let node = Node::builder().with_vault(vault).build(&ctx).await?; + let node = Node::builder().await?.with_vault(vault).build(&ctx).await?; let issuer_identity = hex::decode("81825837830101583285f68200815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef4f41a654cf97d1a7818fc7d8200815840650c4c939b96142546559aed99c52b64aa8a2f7b242b46534f7f8d0c5cc083d2c97210b93e9bca990e9cb9301acc2b634ffb80be314025f9adc870713e6fde0d").unwrap(); let issuer = node.import_private_identity(None, &issuer_identity, &secret).await?; @@ -48,14 +48,14 @@ async fn main(ctx: Context) -> Result<()> { // For a different application this attested attribute set can be different and // distinct for each identifier, but for this example we'll keep things simple. let credential_issuer = CredentialsIssuer::new( - node.identities().repository(), + node.identities().identity_attributes_repository(), node.credentials(), &issuer, "trust_context".into(), ); for identifier in known_identifiers.iter() { node.identities() - .repository() + .identity_attributes_repository() .put_attribute_value(identifier, b"cluster".to_vec(), b"production".to_vec()) .await?; } diff --git a/examples/rust/get_started/examples/06-credentials-exchange-server.rs b/examples/rust/get_started/examples/06-credentials-exchange-server.rs index 4756884845d..c00576c59e3 100644 --- a/examples/rust/get_started/examples/06-credentials-exchange-server.rs +++ b/examples/rust/get_started/examples/06-credentials-exchange-server.rs @@ -14,7 +14,7 @@ use ockam_vault::{EdDSACurve25519SecretKey, SigningSecret, SoftwareVaultForSigni #[ockam::node] async fn main(ctx: Context) -> Result<()> { - let identity_vault = SoftwareVaultForSigning::create(); + let identity_vault = SoftwareVaultForSigning::create().await?; // Import the signing secret key to the Vault let secret = identity_vault .import_key(SigningSecret::EdDSACurve25519(EdDSACurve25519SecretKey::new( @@ -26,10 +26,10 @@ async fn main(ctx: Context) -> Result<()> { .await?; // Create a default Vault but use the signing vault with our secret in it - let mut vault = Vault::create(); + let mut vault = Vault::create().await?; vault.identity_vault = identity_vault; - let node = Node::builder().with_vault(vault).build(&ctx).await?; + let node = Node::builder().await?.with_vault(vault).build(&ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; @@ -90,7 +90,7 @@ async fn main(ctx: Context) -> Result<()> { DefaultAddress::ECHO_SERVICE, &sc_listener_options.spawner_flow_control_id(), ); - let allow_production = AbacAccessControl::create(node.identities_repository(), "cluster", "production"); + let allow_production = AbacAccessControl::create(node.identity_attributes_repository(), "cluster", "production"); node.start_worker_with_access_control(DefaultAddress::ECHO_SERVICE, Echoer, allow_production, AllowAll) .await?; diff --git a/examples/rust/get_started/examples/09-streams-initiator.rs b/examples/rust/get_started/examples/09-streams-initiator.rs index 5c05cd24718..35af124eb40 100644 --- a/examples/rust/get_started/examples/09-streams-initiator.rs +++ b/examples/rust/get_started/examples/09-streams-initiator.rs @@ -4,7 +4,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Set the address of the Kafka node you created here. (e.g. "192.0.2.1:4000") diff --git a/examples/rust/get_started/examples/09-streams-responder.rs b/examples/rust/get_started/examples/09-streams-responder.rs index e2b8904393d..dc61fa015e2 100644 --- a/examples/rust/get_started/examples/09-streams-responder.rs +++ b/examples/rust/get_started/examples/09-streams-responder.rs @@ -5,7 +5,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Start an echoer worker diff --git a/examples/rust/get_started/examples/10-secure-channel-via-streams-initiator.rs b/examples/rust/get_started/examples/10-secure-channel-via-streams-initiator.rs index f37d338d646..7e5ce6696b7 100644 --- a/examples/rust/get_started/examples/10-secure-channel-via-streams-initiator.rs +++ b/examples/rust/get_started/examples/10-secure-channel-via-streams-initiator.rs @@ -5,7 +5,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Set the address of the Kafka node you created here. (e.g. "192.0.2.1:4000") diff --git a/examples/rust/get_started/examples/10-secure-channel-via-streams-responder.rs b/examples/rust/get_started/examples/10-secure-channel-via-streams-responder.rs index 6b7b7b76435..fae7db85cd1 100644 --- a/examples/rust/get_started/examples/10-secure-channel-via-streams-responder.rs +++ b/examples/rust/get_started/examples/10-secure-channel-via-streams-responder.rs @@ -6,7 +6,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Start an echoer worker diff --git a/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs b/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs index cd7935287e7..05f473b184e 100644 --- a/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs +++ b/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs @@ -1,10 +1,12 @@ +use std::sync::Arc; + use hello_ockam::{create_token, import_project}; use ockam::abac::AbacAccessControl; -use ockam::identity::OneTimeCode; use ockam::identity::{ AuthorityService, RemoteCredentialsRetriever, RemoteCredentialsRetrieverInfo, SecureChannelListenerOptions, SecureChannelOptions, TrustContext, TrustMultiIdentifiersPolicy, }; +use ockam::identity::{CredentialsRetriever, OneTimeCode}; use ockam::remote::RemoteRelayOptions; use ockam::{node, route, Context, Result, TcpOutletOptions}; use ockam_api::authenticator::enrollment_tokens::TokenAcceptor; @@ -12,7 +14,6 @@ use ockam_api::nodes::NodeManager; use ockam_api::{multiaddr_to_route, DefaultAddress}; use ockam_multiaddr::MultiAddr; use ockam_transport_tcp::TcpTransportExtension; -use std::sync::Arc; /// This node supports a "control" server on which several "edge" devices can connect /// @@ -49,7 +50,7 @@ async fn main(ctx: Context) -> Result<()> { /// start the control node async fn start_node(ctx: Context, project_information_path: &str, token: OneTimeCode) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the TCP transport let tcp = node.create_tcp_transport().await?; @@ -74,26 +75,24 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime let tcp_project_session = multiaddr_to_route(&project.authority_route(), &tcp).await.unwrap(); // FIXME: Handle error // Create a trust context that will be used to authenticate credential exchanges + let credentials_retriever = Arc::new(RemoteCredentialsRetriever::new( + node.secure_channels(), + RemoteCredentialsRetrieverInfo::new( + project.authority_identifier(), + tcp_project_session.route, + DefaultAddress::CREDENTIAL_ISSUER.into(), + ), + )); let trust_context = TrustContext::new( "trust_context_id".to_string(), Some(AuthorityService::new( node.credentials(), project.authority_identifier(), - Some(Arc::new(RemoteCredentialsRetriever::new( - node.secure_channels(), - RemoteCredentialsRetrieverInfo::new( - project.authority_identifier(), - tcp_project_session.route, - DefaultAddress::CREDENTIAL_ISSUER.into(), - ), - ))), + Some(credentials_retriever.clone()), )), ); - let credential = trust_context - .authority()? - .credential(node.context(), &control_plane) - .await?; + let credential = credentials_retriever.retrieve(node.context(), &control_plane).await?; // start a credential exchange worker which will be // later on to exchange credentials with the edge node @@ -108,7 +107,7 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime .await?; // 3. create an access control policy checking the value of the "component" attribute of the caller - let access_control = AbacAccessControl::create(node.identities_repository(), "component", "edge"); + let access_control = AbacAccessControl::create(node.identity_attributes_repository(), "component", "edge"); // 4. create a tcp outlet with the above policy tcp.create_outlet( diff --git a/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs b/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs index c6d8686e8be..dff4efb9aab 100644 --- a/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs +++ b/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs @@ -1,10 +1,10 @@ use hello_ockam::{create_token, import_project}; use ockam::abac::AbacAccessControl; -use ockam::identity::OneTimeCode; use ockam::identity::{ identities, AuthorityService, RemoteCredentialsRetriever, RemoteCredentialsRetrieverInfo, SecureChannelOptions, TrustContext, TrustMultiIdentifiersPolicy, }; +use ockam::identity::{CredentialsRetriever, OneTimeCode}; use ockam::node; use ockam::{route, Context, Result}; use ockam_api::authenticator::enrollment_tokens::TokenAcceptor; @@ -48,7 +48,7 @@ async fn main(ctx: Context) -> Result<()> { /// start the edge node async fn start_node(ctx: Context, project_information_path: &str, token: OneTimeCode) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Use the TCP transport let tcp = node.create_tcp_transport().await?; @@ -74,26 +74,25 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime // Create a trust context that will be used to authenticate credential exchanges let tcp_project_session = multiaddr_to_route(&project.route(), &tcp).await.unwrap(); // FIXME: Handle error + // Create a trust context that will be used to authenticate credential exchanges + let credentials_retriever = Arc::new(RemoteCredentialsRetriever::new( + node.secure_channels(), + RemoteCredentialsRetrieverInfo::new( + project.authority_identifier(), + tcp_project_session.route, + DefaultAddress::CREDENTIAL_ISSUER.into(), + ), + )); let trust_context = TrustContext::new( "trust_context_id".to_string(), Some(AuthorityService::new( node.credentials(), project.authority_identifier(), - Some(Arc::new(RemoteCredentialsRetriever::new( - node.secure_channels(), - RemoteCredentialsRetrieverInfo::new( - project.authority_identifier(), - tcp_project_session.route, - DefaultAddress::CREDENTIAL_ISSUER.into(), - ), - ))), + Some(credentials_retriever.clone()), )), ); - let credential = trust_context - .authority()? - .credential(node.context(), &edge_plane) - .await?; + let credential = credentials_retriever.retrieve(node.context(), &edge_plane).await?; // start a credential exchange worker which will be // later on to exchange credentials with the control node @@ -108,7 +107,11 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime .await?; // 3. create an access control policy checking the value of the "component" attribute of the caller - let access_control = AbacAccessControl::create(identities().repository(), "component", "control"); + let access_control = AbacAccessControl::create( + identities().await?.identity_attributes_repository(), + "component", + "control", + ); // 4. create a tcp inlet with the above policy diff --git a/examples/rust/get_started/examples/alice.rs b/examples/rust/get_started/examples/alice.rs index d344ae13756..4def6fd1796 100644 --- a/examples/rust/get_started/examples/alice.rs +++ b/examples/rust/get_started/examples/alice.rs @@ -6,7 +6,7 @@ use std::io; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/bob.rs b/examples/rust/get_started/examples/bob.rs index 5651c6fb051..3da09e07968 100644 --- a/examples/rust/get_started/examples/bob.rs +++ b/examples/rust/get_started/examples/bob.rs @@ -24,7 +24,7 @@ impl Worker for Echoer { #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let node = node(ctx); + let node = node(ctx).await?; // Initialize the TCP Transport let tcp = node.create_tcp_transport().await?; diff --git a/examples/rust/get_started/examples/hello.rs b/examples/rust/get_started/examples/hello.rs index 179685346c2..ec2fce7f68e 100644 --- a/examples/rust/get_started/examples/hello.rs +++ b/examples/rust/get_started/examples/hello.rs @@ -4,7 +4,7 @@ use ockam::{node, route, Context, Result}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create a node with default implementations - let mut node = node(ctx); + let mut node = node(ctx).await?; // Create an Identity to represent Bob let bob = node.create_identity().await?; diff --git a/examples/rust/get_started/examples/vault-and-identities.rs b/examples/rust/get_started/examples/vault-and-identities.rs index a9f26fc9dd8..36e3adaf5ca 100644 --- a/examples/rust/get_started/examples/vault-and-identities.rs +++ b/examples/rust/get_started/examples/vault-and-identities.rs @@ -4,7 +4,7 @@ use ockam::{Context, Result}; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Create default node to safely store secret keys for Alice - let mut node = node(ctx); + let mut node = node(ctx).await?; // Create an Identity to represent Alice. let _alice = node.create_identity().await?; diff --git a/examples/rust/get_started/src/token.rs b/examples/rust/get_started/src/token.rs index 6219f6128c7..2840292dc1b 100644 --- a/examples/rust/get_started/src/token.rs +++ b/examples/rust/get_started/src/token.rs @@ -1,11 +1,13 @@ +use std::process::Command; +use std::str; + use anyhow::anyhow; + use ockam::identity::OneTimeCode; use ockam::Result; -use ockam_api::identity::EnrollmentTicket; +use ockam_api::cli_state::enrollments::EnrollmentTicket; use ockam_core::errcode::{Kind, Origin}; use ockam_core::Error; -use std::process::Command; -use std::str; /// Invoke the `ockam` command line in order to create a one-time token for /// a given attribute name/value (and the default project on this machine) diff --git a/examples/rust/ockam_kafka/examples/ockam_kafka_alice.rs b/examples/rust/ockam_kafka/examples/ockam_kafka_alice.rs index daf83932015..f36a90f3b49 100644 --- a/examples/rust/ockam_kafka/examples/ockam_kafka_alice.rs +++ b/examples/rust/ockam_kafka/examples/ockam_kafka_alice.rs @@ -6,7 +6,7 @@ use std::io; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let mut node = node(ctx); + let mut node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Create an Identity to represent Alice. diff --git a/examples/rust/ockam_kafka/examples/ockam_kafka_bob.rs b/examples/rust/ockam_kafka/examples/ockam_kafka_bob.rs index 627b91979a8..87ff45a5a6c 100644 --- a/examples/rust/ockam_kafka/examples/ockam_kafka_bob.rs +++ b/examples/rust/ockam_kafka/examples/ockam_kafka_bob.rs @@ -22,7 +22,7 @@ impl Worker for Echoer { #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Create an Identity to represent Bob. diff --git a/examples/rust/tcp_inlet_and_outlet/examples/01-inlet-outlet.rs b/examples/rust/tcp_inlet_and_outlet/examples/01-inlet-outlet.rs index f30052dd2b7..40167bfa20a 100644 --- a/examples/rust/tcp_inlet_and_outlet/examples/01-inlet-outlet.rs +++ b/examples/rust/tcp_inlet_and_outlet/examples/01-inlet-outlet.rs @@ -4,7 +4,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Expect second command line argument to be the TCP address of a target TCP server. diff --git a/examples/rust/tcp_inlet_and_outlet/examples/02-inlet.rs b/examples/rust/tcp_inlet_and_outlet/examples/02-inlet.rs index 109d6dcd501..b8609300297 100644 --- a/examples/rust/tcp_inlet_and_outlet/examples/02-inlet.rs +++ b/examples/rust/tcp_inlet_and_outlet/examples/02-inlet.rs @@ -4,7 +4,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // We know that the Outlet node is listening for Ockam Routing Messages diff --git a/examples/rust/tcp_inlet_and_outlet/examples/02-outlet.rs b/examples/rust/tcp_inlet_and_outlet/examples/02-outlet.rs index bcbf0a6b593..a35521569bf 100644 --- a/examples/rust/tcp_inlet_and_outlet/examples/02-outlet.rs +++ b/examples/rust/tcp_inlet_and_outlet/examples/02-outlet.rs @@ -4,7 +4,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Expect first command line argument to be the TCP address of a target TCP server. diff --git a/examples/rust/tcp_inlet_and_outlet/examples/03-inlet.rs b/examples/rust/tcp_inlet_and_outlet/examples/03-inlet.rs index b1e15b11ae8..a064a5e53da 100644 --- a/examples/rust/tcp_inlet_and_outlet/examples/03-inlet.rs +++ b/examples/rust/tcp_inlet_and_outlet/examples/03-inlet.rs @@ -5,7 +5,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; let e = node.create_identity().await?; diff --git a/examples/rust/tcp_inlet_and_outlet/examples/03-outlet.rs b/examples/rust/tcp_inlet_and_outlet/examples/03-outlet.rs index a34b0dcf002..27cf2afd27c 100644 --- a/examples/rust/tcp_inlet_and_outlet/examples/03-outlet.rs +++ b/examples/rust/tcp_inlet_and_outlet/examples/03-outlet.rs @@ -5,7 +5,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Create: diff --git a/examples/rust/tcp_inlet_and_outlet/examples/04-inlet.rs b/examples/rust/tcp_inlet_and_outlet/examples/04-inlet.rs index 84dcba7ecb2..1d9f8200647 100644 --- a/examples/rust/tcp_inlet_and_outlet/examples/04-inlet.rs +++ b/examples/rust/tcp_inlet_and_outlet/examples/04-inlet.rs @@ -5,7 +5,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; // Create a Vault to store our cryptographic keys and an Identity to represent this Node. diff --git a/examples/rust/tcp_inlet_and_outlet/examples/04-outlet.rs b/examples/rust/tcp_inlet_and_outlet/examples/04-outlet.rs index bc3a3dd45a2..32f9d279c95 100644 --- a/examples/rust/tcp_inlet_and_outlet/examples/04-outlet.rs +++ b/examples/rust/tcp_inlet_and_outlet/examples/04-outlet.rs @@ -6,7 +6,7 @@ use ockam_transport_tcp::TcpTransportExtension; #[ockam::node] async fn main(ctx: Context) -> Result<()> { // Initialize the TCP Transport. - let node = node(ctx); + let node = node(ctx).await?; let tcp = node.create_tcp_transport().await?; let e = node.create_identity().await?; diff --git a/implementations/elixir/ockam/ockly/native/ockly/Cargo.lock b/implementations/elixir/ockam/ockly/native/ockly/Cargo.lock index 07208cbbd2e..b5adff1f158 100644 --- a/implementations/elixir/ockam/ockly/native/ockly/Cargo.lock +++ b/implementations/elixir/ockam/ockly/native/ockly/Cargo.lock @@ -60,6 +60,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -74,6 +75,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "arrayref" version = "0.3.7" @@ -91,6 +98,15 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-polyfill" version = "0.1.11" @@ -483,6 +499,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -529,6 +554,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "num-traits", +] + [[package]] name = "cipher" version = "0.3.0" @@ -578,6 +612,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "critical-section" version = "1.1.2" @@ -705,6 +754,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dyn-clone" version = "1.0.16" @@ -755,6 +810,9 @@ name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -782,6 +840,33 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "2.0.1" @@ -804,6 +889,23 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -859,6 +961,28 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.29" @@ -967,19 +1091,13 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "hash32" version = "0.2.1" @@ -996,9 +1114,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ "ahash", + "allocator-api2", "serde", ] +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.0", +] + [[package]] name = "heapless" version = "0.7.16" @@ -1017,6 +1145,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -1048,6 +1179,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "http" version = "0.2.11" @@ -1122,6 +1262,16 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -1132,6 +1282,25 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indexmap" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1143,6 +1312,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "leb128" @@ -1157,28 +1329,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] -name = "lmdb-rkv" -version = "0.14.0" +name = "libm" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447a296f7aca299cfbb50f4e4f3d49451549af655fb7215d7f8c0c3d64bad42b" -dependencies = [ - "bitflags", - "byteorder", - "libc", - "lmdb-rkv-sys", -] +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] -name = "lmdb-rkv-sys" -version = "0.11.2" +name = "libsqlite3-sys" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b9ce6b3be08acefa3003c57b7565377432a89ec24476bbe72e11d101f852fe" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", - "libc", "pkg-config", + "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + [[package]] name = "lock_api" version = "0.4.11" @@ -1204,6 +1376,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.6.4" @@ -1230,6 +1412,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1250,6 +1438,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1260,6 +1458,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1270,6 +1485,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -1277,6 +1503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1380,11 +1607,11 @@ version = "0.93.0" dependencies = [ "async-trait", "cfg-if", + "chrono", "delegate", "group", "heapless", "hex", - "lmdb-rkv", "minicbor", "ockam_core", "ockam_macros", @@ -1395,6 +1622,7 @@ dependencies = [ "serde-big-array", "serde_bare", "sha2", + "sqlx", "subtle", "time", "tokio-retry", @@ -1425,7 +1653,10 @@ dependencies = [ "serde", "serde_bare", "serde_json", + "sqlx", + "time", "tokio", + "tokio-retry", "tracing", "tracing-error", "tracing-subscriber", @@ -1473,8 +1704,8 @@ dependencies = [ "p256", "rand", "serde", - "serde_cbor", "sha2", + "sqlx", "static_assertions", "tracing", "x25519-dalek", @@ -1556,6 +1787,35 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1603,6 +1863,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -1706,6 +1977,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.10.2" @@ -1774,6 +2054,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rsa" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1789,6 +2089,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustler" version = "0.29.1" @@ -1916,7 +2229,7 @@ version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -1966,16 +2279,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.193" @@ -1998,6 +2301,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2082,6 +2396,211 @@ dependencies = [ "der", ] +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.0.1", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2100,6 +2619,17 @@ version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75b72ee54e2f93c3ea1354066162be893ee5e25773ab743de3e088cecbb4f31" +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "subtle" version = "2.5.0" @@ -2128,6 +2658,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -2354,12 +2897,39 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unicode-width" version = "0.1.11" @@ -2391,6 +2961,17 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "urlencoding" version = "2.1.3" @@ -2409,6 +2990,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -2463,6 +3050,12 @@ dependencies = [ "wasm-encoder", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "winapi" version = "0.3.9" diff --git a/implementations/elixir/ockam/ockly/native/ockly/src/lib.rs b/implementations/elixir/ockam/ockly/native/ockly/src/lib.rs index bcc4eeb993d..02283f9dc18 100644 --- a/implementations/elixir/ockam/ockly/native/ockly/src/lib.rs +++ b/implementations/elixir/ockam/ockly/native/ockly/src/lib.rs @@ -88,17 +88,22 @@ fn identities_ref() -> NifResult> { } fn load_memory_vault() -> bool { - let identity_vault = SoftwareVaultForSigning::create(); - let secure_channel_vault = SoftwareVaultForSecureChannels::create(); - *IDENTITY_MEMORY_VAULT.write().unwrap() = Some(identity_vault.clone()); - *SECURE_CHANNEL_MEMORY_VAULT.write().unwrap() = Some(secure_channel_vault.clone()); - let builder = ockam_identity::Identities::builder().with_vault(Vault::new( - identity_vault, - secure_channel_vault, - Vault::create_credential_vault(), - Vault::create_verifying_vault(), - )); - *IDENTITIES.write().unwrap() = Some(builder.build()); + block_future(async move { + let identity_vault = SoftwareVaultForSigning::create().await.unwrap(); + let secure_channel_vault = SoftwareVaultForSecureChannels::create().await.unwrap(); + *IDENTITY_MEMORY_VAULT.write().unwrap() = Some(identity_vault.clone()); + *SECURE_CHANNEL_MEMORY_VAULT.write().unwrap() = Some(secure_channel_vault.clone()); + let builder = ockam_identity::Identities::builder() + .await + .unwrap() + .with_vault(Vault::new( + identity_vault, + secure_channel_vault, + Vault::create_credential_vault().await.unwrap(), + Vault::create_verifying_vault(), + )); + *IDENTITIES.write().unwrap() = Some(builder.build()); + }); true } @@ -123,12 +128,15 @@ fn setup_aws_kms(key_ids: Vec) -> NifResult { match AwsSigningVault::create_with_config(config).await { Ok(vault) => { let aws_vault = Arc::new(vault); - let builder = ockam_identity::Identities::builder().with_vault(Vault::new( - aws_vault.clone(), - secure_channel_vault, - aws_vault, - Vault::create_verifying_vault(), - )); + let builder = ockam_identity::Identities::builder() + .await + .map_err(|e| Error::Term(Box::new(e.to_string())))? + .with_vault(Vault::new( + aws_vault.clone(), + secure_channel_vault, + aws_vault, + Vault::create_verifying_vault(), + )); *IDENTITIES.write().unwrap() = Some(builder.build()); Ok(true) } diff --git a/implementations/rust/ockam/ockam/Cargo.toml b/implementations/rust/ockam/ockam/Cargo.toml index 2fb5cba023e..a016247f285 100644 --- a/implementations/rust/ockam/ockam/Cargo.toml +++ b/implementations/rust/ockam/ockam/Cargo.toml @@ -31,9 +31,9 @@ all-features = false rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["std", "ockam_transport_tcp", "software_vault_storage"] +default = ["std", "ockam_transport_tcp", "storage"] software_vault = ["ockam_identity/software_vault"] -software_vault_storage = ["software_vault", "ockam_vault/storage"] +storage = ["ockam_identity/storage"] OCKAM_XX_25519_AES256_GCM_SHA256 = ["ockam_identity/OCKAM_XX_25519_AES256_GCM_SHA256"] OCKAM_XX_25519_AES128_GCM_SHA256 = ["ockam_identity/OCKAM_XX_25519_AES128_GCM_SHA256"] OCKAM_XX_25519_ChaChaPolyBLAKE2s = ["ockam_identity/OCKAM_XX_25519_ChaChaPolyBLAKE2s"] diff --git a/implementations/rust/ockam/ockam/src/lib.rs b/implementations/rust/ockam/ockam/src/lib.rs index 90cb3dfd3e9..6578eb7590f 100644 --- a/implementations/rust/ockam/ockam/src/lib.rs +++ b/implementations/rust/ockam/ockam/src/lib.rs @@ -43,26 +43,56 @@ )] #![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "std")] -extern crate core; - #[cfg(feature = "alloc")] #[macro_use] extern crate alloc; - +#[cfg(feature = "std")] +extern crate core; #[macro_use] extern crate tracing; +pub use error::OckamError; +pub use metadata::OckamMessage; +pub use node::*; +#[cfg(feature = "std")] +pub use ockam_abac as abac; +/// Mark an Ockam Processor implementation. +/// +/// This is currently implemented as a re-export of the `async_trait` macro, but +/// may be changed in the future to a [`Processor`](crate::Processor)-specific macro. +pub use ockam_core::processor; +/// Mark an Ockam Worker implementation. +/// +/// This is currently implemented as a re-export of the `async_trait` macro, but +/// may be changed in the future to a [`Worker`](crate::Worker)-specific macro. +pub use ockam_core::worker; +pub use ockam_core::{ + allow, deny, errcode, route, Address, Any, AsyncTryClone, Encoded, Error, LocalMessage, + Mailbox, Mailboxes, Message, Processor, ProtocolId, Result, Route, Routed, TransportMessage, + Worker, +}; +pub use ockam_identity as identity; // --- // Export the ockam macros that aren't coming from ockam_core. pub use ockam_macros::{node, test}; -// --- - // Export node implementation +#[cfg(feature = "std")] +pub use ockam_node::database::*; pub use ockam_node::{ debugger, Context, DelayedEvent, Executor, MessageReceiveOptions, MessageSendReceiveOptions, NodeBuilder, WorkerBuilder, }; +#[cfg(feature = "ockam_transport_tcp")] +pub use ockam_transport_tcp::{ + TcpConnectionOptions, TcpInletOptions, TcpListenerOptions, TcpOutletOptions, TcpTransport, + TcpTransportExtension, +}; +pub use relay_service::{RelayService, RelayServiceOptions}; +pub use system::{SystemBuilder, SystemHandler, WorkerSystem}; +pub use unique::unique_with_prefix; + +// --- + // --- mod delay; @@ -73,12 +103,6 @@ mod relay_service; mod system; mod unique; -pub use error::OckamError; -pub use metadata::OckamMessage; -pub use relay_service::{RelayService, RelayServiceOptions}; -pub use system::{SystemBuilder, SystemHandler, WorkerSystem}; -pub use unique::unique_with_prefix; - pub mod channel; pub mod pipe; pub mod pipe2; @@ -87,18 +111,6 @@ pub mod remote; pub mod stream; pub mod workers; -#[cfg(feature = "std")] -pub use ockam_abac as abac; -pub use ockam_identity as identity; -#[cfg(feature = "std")] -pub use ockam_identity::storage::lmdb_storage::*; - -pub use ockam_core::{ - allow, deny, errcode, route, Address, Any, AsyncTryClone, Encoded, Error, LocalMessage, - Mailbox, Mailboxes, Message, Processor, ProtocolId, Result, Route, Routed, TransportMessage, - Worker, -}; - /// Access Control pub mod access_control { pub use ockam_core::access_control::*; @@ -110,18 +122,6 @@ pub mod flow_control { pub use ockam_core::flow_control::*; } -/// Mark an Ockam Worker implementation. -/// -/// This is currently implemented as a re-export of the `async_trait` macro, but -/// may be changed in the future to a [`Worker`](crate::Worker)-specific macro. -pub use ockam_core::worker; - -/// Mark an Ockam Processor implementation. -/// -/// This is currently implemented as a re-export of the `async_trait` macro, but -/// may be changed in the future to a [`Processor`](crate::Processor)-specific macro. -pub use ockam_core::processor; - // TODO: think about how to handle this more. Probably extract these into an // `ockam_compat` crate. pub mod compat { @@ -138,20 +138,12 @@ pub mod vault { //! Types and traits relating to ockam vaults. pub use ockam_vault::*; - #[cfg(feature = "software_vault_storage")] + #[cfg(feature = "storage")] /// Storage pub mod storage { pub use ockam_vault::storage::*; } } -#[cfg(feature = "ockam_transport_tcp")] -pub use ockam_transport_tcp::{ - TcpConnectionOptions, TcpInletOptions, TcpListenerOptions, TcpOutletOptions, TcpTransport, - TcpTransportExtension, -}; - /// List of all top-level services pub mod node; - -pub use node::*; diff --git a/implementations/rust/ockam/ockam/src/node.rs b/implementations/rust/ockam/ockam/src/node.rs index e119dac9985..7137870ceeb 100644 --- a/implementations/rust/ockam/ockam/src/node.rs +++ b/implementations/rust/ockam/ockam/src/node.rs @@ -1,11 +1,3 @@ -use crate::identity::models::Identifier; -use crate::identity::storage::Storage; -use crate::identity::{ - secure_channels, Credentials, CredentialsServer, Identities, IdentitiesCreation, - IdentitiesKeys, IdentitiesRepository, SecureChannel, SecureChannelListener, - SecureChannelRegistry, SecureChannels, SecureChannelsBuilder, -}; -use crate::identity::{SecureChannelListenerOptions, SecureChannelOptions}; use ockam_core::compat::string::String; use ockam_core::compat::sync::Arc; use ockam_core::flow_control::FlowControls; @@ -13,10 +5,20 @@ use ockam_core::{ Address, AsyncTryClone, IncomingAccessControl, Message, OutgoingAccessControl, Processor, Result, Route, Routed, Worker, }; -use ockam_identity::{PurposeKeys, Vault, VaultStorage}; +use ockam_identity::{IdentityAttributesRepository, PurposeKeys, Vault}; use ockam_node::{Context, HasContext, MessageReceiveOptions, MessageSendReceiveOptions}; +use ockam_vault::storage::SecretsRepository; use ockam_vault::SigningSecretKeyHandle; +use crate::identity::models::Identifier; +#[cfg(feature = "storage")] +use crate::identity::secure_channels; +use crate::identity::{ + ChangeHistoryRepository, Credentials, CredentialsServer, Identities, IdentitiesCreation, + IdentitiesKeys, SecureChannel, SecureChannelListener, SecureChannelRegistry, SecureChannels, + SecureChannelsBuilder, +}; +use crate::identity::{SecureChannelListenerOptions, SecureChannelOptions}; use crate::remote::{RemoteRelay, RemoteRelayInfo, RemoteRelayOptions}; use crate::stream::Stream; use crate::OckamError; @@ -36,33 +38,21 @@ pub struct Node { /// use std::sync::Arc; /// use ockam::{Node, Result}; /// use ockam_node::Context; -/// use ockam_vault::storage::PersistentStorage; +/// use ockam_vault::storage::SecretsSqlxDatabase; /// /// async fn make_node(ctx: Context) -> Result { -/// let node = Node::builder().with_vault_storage(PersistentStorage::create(Path::new("vault")).await?).build(&ctx).await?; +/// let node = Node::builder().await?.with_secrets_repository(SecretsSqlxDatabase::create().await?).build(&ctx).await?; /// Ok(node) /// } /// /// /// ``` -/// Here is another example where we specify a local LMDB database to store identity attributes -/// ```rust -/// use std::sync::Arc; -/// use ockam::{Node, Result}; -/// use ockam::LmdbStorage; -/// use ockam_node::Context; -/// -/// async fn make_node(ctx: Context) -> Result { -/// let lmdb_storage = Arc::new(LmdbStorage::new("identities").await?); -/// let node = Node::builder().with_identities_storage(lmdb_storage).build(&ctx).await?; -/// Ok(node) -/// } -/// ``` -pub fn node(ctx: Context) -> Node { - Node { +#[cfg(feature = "storage")] +pub async fn node(ctx: Context) -> Result { + Ok(Node { context: ctx, - secure_channels: secure_channels(), - } + secure_channels: secure_channels().await?, + }) } impl Node { @@ -309,13 +299,23 @@ impl Node { } /// Return the repository used to store identities data - pub fn identities_repository(&self) -> Arc { - self.secure_channels.identities().repository() + pub fn identities_repository(&self) -> Arc { + self.secure_channels + .identities() + .change_history_repository() + } + + /// Return the repository used to store identities attributes + pub fn identity_attributes_repository(&self) -> Arc { + self.secure_channels + .identities() + .identity_attributes_repository() } /// Return a new builder for top-level services - pub fn builder() -> NodeBuilder { - NodeBuilder::new() + #[cfg(feature = "storage")] + pub async fn builder() -> Result { + NodeBuilder::new().await } } @@ -335,10 +335,11 @@ pub struct NodeBuilder { } impl NodeBuilder { - fn new() -> Self { - Self { - builder: SecureChannels::builder(), - } + #[cfg(feature = "storage")] + async fn new() -> Result { + Ok(Self { + builder: SecureChannels::builder().await?, + }) } /// Set [`Vault`] @@ -347,21 +348,27 @@ impl NodeBuilder { self } - /// With Software Vault with given Storage - pub fn with_vault_storage(mut self, storage: VaultStorage) -> Self { - self.builder = self.builder.with_vault_storage(storage); + /// With Software Vault with given secrets repository + pub fn with_secrets_repository(mut self, repository: Arc) -> Self { + self.builder = self.builder.with_secrets_repository(repository); self } - /// Set a specific storage for identities - pub fn with_identities_storage(mut self, storage: Arc) -> Self { - self.builder = self.builder.with_identities_storage(storage); + /// Set a specific change history repository + pub fn with_change_history_repository( + mut self, + repository: Arc, + ) -> Self { + self.builder = self.builder.with_change_history_repository(repository); self } - /// Set a specific identities repository - pub fn with_identities_repository(mut self, repository: Arc) -> Self { - self.builder = self.builder.with_identities_repository(repository); + /// Set a specific identity attributes repository + pub fn with_identity_attributes_repository( + mut self, + repository: Arc, + ) -> Self { + self.builder = self.builder.with_identity_attributes_repository(repository); self } diff --git a/implementations/rust/ockam/ockam/tests/relay.rs b/implementations/rust/ockam/ockam/tests/relay.rs index 5b24f921231..262cd718260 100644 --- a/implementations/rust/ockam/ockam/tests/relay.rs +++ b/implementations/rust/ockam/ockam/tests/relay.rs @@ -164,7 +164,7 @@ async fn test4(ctx: &mut Context) -> Result<()> { .relay_as_consumer(&cloud_secure_channel_listener_options.spawner_flow_control_id()); RelayService::create(ctx, "forwarding_service", options).await?; - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let cloud = identities_creation.create_identity().await?; secure_channels diff --git a/implementations/rust/ockam/ockam_abac/Cargo.toml b/implementations/rust/ockam/ockam_abac/Cargo.toml index 6b718f0d379..0631da08c30 100644 --- a/implementations/rust/ockam/ockam_abac/Cargo.toml +++ b/implementations/rust/ockam/ockam_abac/Cargo.toml @@ -20,31 +20,31 @@ repl = ["rustyline", "rustyline-derive", "std"] std = [ "ockam_core/std", "ockam_identity/std", + "ockam_node/std", "minicbor/std", "tracing/std", "either/use_std", - "lmdb", "once_cell/std", + "sqlx", "regex", "tokio", "wast", ] -lmdb = ["tokio", "lmdb-rkv"] -sqlite = ["rusqlite"] [dependencies] either = { version = "1.9.0", default-features = false } -lmdb-rkv = { version = "0.14.0", optional = true } minicbor = { version = "0.20.0", features = ["derive", "alloc"] } ockam_core = { version = "0.93.0", path = "../ockam_core", default-features = false } ockam_executor = { version = "0.61.0", path = "../ockam_executor", default-features = false } ockam_identity = { version = "0.93.0", path = "../ockam_identity", default-features = false } +ockam_node = { version = "0.98.0", path = "../ockam_node", default-features = false } once_cell = { version = "1.18.0", default-features = false, features = ["alloc"] } # optional: regex = { version = "1.10.2", default-features = false, optional = true } rusqlite = { version = "0.30.0", optional = true } rustyline = { version = "12.0.0", optional = true } rustyline-derive = { version = "0.9.0", optional = true } +sqlx = { version = "0.7.3", optional = true } str-buf = "3.0.1" tokio = { version = "1.34", default-features = false, optional = true, features = ["sync", "time", "rt", "rt-multi-thread", "macros"] } tracing = { version = "0.1", default-features = false } diff --git a/implementations/rust/ockam/ockam_abac/src/attribute_access_control.rs b/implementations/rust/ockam/ockam_abac/src/attribute_access_control.rs index 85dba10c91d..def21de5063 100644 --- a/implementations/rust/ockam/ockam_abac/src/attribute_access_control.rs +++ b/implementations/rust/ockam/ockam_abac/src/attribute_access_control.rs @@ -16,14 +16,14 @@ use crate::{eval, Env, Expr}; use ockam_core::compat::format; use ockam_core::compat::string::ToString; use ockam_core::compat::sync::Arc; -use ockam_identity::{Identifier, IdentitiesRepository, IdentitySecureChannelLocalInfo}; +use ockam_identity::{Identifier, IdentityAttributesRepository, IdentitySecureChannelLocalInfo}; /// This AccessControl uses a storage for authenticated attributes in order /// to verify if a policy expression is valid /// A similar access control policy is available as [`crate::policy::PolicyAccessControl`] where -/// as [`crate::PolicyStorage`] can be used to retrieve a specific policy for a given resource and action +/// as [`crate::PoliciesRepository`] can be used to retrieve a specific policy for a given resource and action pub struct AbacAccessControl { - repository: Arc, + identity_attributes_repository: Arc, expression: Expr, environment: Env, } @@ -39,12 +39,12 @@ impl Debug for AbacAccessControl { impl AbacAccessControl { /// Create a new AccessControl using a specific policy for checking attributes pub fn new( - repository: Arc, + identity_attributes_repository: Arc, expression: Expr, environment: Env, ) -> Self { Self { - repository, + identity_attributes_repository, expression, environment, } @@ -53,7 +53,7 @@ impl AbacAccessControl { /// Create an AccessControl which will verify that the sender of /// a message has an authenticated attribute with the correct name and value pub fn create( - repository: Arc, + identity_attributes_repository: Arc, attribute_name: &str, attribute_value: &str, ) -> AbacAccessControl @@ -63,7 +63,7 @@ where { Ident(format!("subject.{attribute_name}")), Str(attribute_value.into()), ]); - AbacAccessControl::new(repository, expression, Env::new()) + AbacAccessControl::new(identity_attributes_repository, expression, Env::new()) } } @@ -73,7 +73,11 @@ impl AbacAccessControl { let mut environment = self.environment.clone(); // Get identity attributes and populate the environment: - if let Some(attrs) = self.repository.get_attributes(&id).await? { + if let Some(attrs) = self + .identity_attributes_repository + .get_attributes(&id) + .await? + { for (key, value) in attrs.attrs() { let key = match from_utf8(key) { Ok(key) => key, diff --git a/implementations/rust/ockam/ockam_abac/src/lib.rs b/implementations/rust/ockam/ockam_abac/src/lib.rs index 3894d287c61..6c62a439c1f 100644 --- a/implementations/rust/ockam/ockam_abac/src/lib.rs +++ b/implementations/rust/ockam/ockam_abac/src/lib.rs @@ -10,7 +10,6 @@ mod env; mod error; mod eval; mod policy; -mod traits; mod types; #[cfg(feature = "std")] @@ -18,7 +17,6 @@ mod parser; pub mod attribute_access_control; pub mod expr; -pub mod mem; mod storage; pub use attribute_access_control::AbacAccessControl; @@ -27,7 +25,7 @@ pub use error::{EvalError, ParseError}; pub use eval::eval; pub use expr::Expr; pub use policy::PolicyAccessControl; -pub use traits::PolicyStorage; +pub use storage::*; pub use types::{Action, Resource, Subject}; #[cfg(feature = "std")] diff --git a/implementations/rust/ockam/ockam_abac/src/mem.rs b/implementations/rust/ockam/ockam_abac/src/mem.rs deleted file mode 100644 index 5f5a10f99fe..00000000000 --- a/implementations/rust/ockam/ockam_abac/src/mem.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::expr::Expr; -use crate::traits::PolicyStorage; -use crate::types::{Action, Resource}; -use core::fmt; -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::sync::{Arc, RwLock}; -use ockam_core::compat::vec::Vec; -use ockam_core::Result; - -#[derive(Default)] -pub struct Memory { - pub(crate) inner: Arc>, -} - -impl fmt::Debug for Memory { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Memory") - } -} - -impl Memory { - pub fn new() -> Self { - Self { - inner: Arc::new(RwLock::new(Inner::new())), - } - } -} - -#[derive(Default)] -pub struct Inner { - policies: BTreeMap>, -} - -impl Inner { - fn new() -> Self { - Inner::default() - } - - fn del_policy(&mut self, r: &Resource, a: &Action) { - if let Some(p) = self.policies.get_mut(r) { - p.remove(a); - if p.is_empty() { - self.policies.remove(r); - } - } - } - - fn get_policy(&self, r: &Resource, a: &Action) -> Option { - self.policies.get(r).and_then(|p| p.get(a).cloned()) - } - - fn set_policy(&mut self, r: &Resource, a: &Action, p: &Expr) { - self.policies - .entry(r.clone()) - .or_insert_with(BTreeMap::new) - .insert(a.clone(), p.clone()); - } - - fn policies(&self, r: &Resource) -> Vec<(Action, Expr)> { - if let Some(p) = self.policies.get(r) { - p.iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect::>() - } else { - Vec::new() - } - } -} - -#[async_trait] -impl PolicyStorage for Memory { - async fn del_policy(&self, r: &Resource, a: &Action) -> Result<()> { - self.inner.write().unwrap().del_policy(r, a); - Ok(()) - } - - async fn get_policy(&self, r: &Resource, a: &Action) -> Result> { - Ok(self.inner.read().unwrap().get_policy(r, a)) - } - - async fn set_policy(&self, r: &Resource, a: &Action, p: &Expr) -> Result<()> { - self.inner.write().unwrap().set_policy(r, a, p); - Ok(()) - } - - async fn policies(&self, r: &Resource) -> Result> { - Ok(self.inner.write().unwrap().policies(r)) - } -} - -#[cfg(test)] -mod tests { - use crate::env::Env; - use crate::eval::eval; - use crate::expr::{int, seq, str}; - use crate::mem::Memory; - use crate::parser::parse; - use crate::types::{Action, Resource}; - - #[test] - fn example1() { - let condition = r#" - (and (= resource.version "1.0.0") - (= subject.name "John") - (member? "John" resource.admins)) - "#; - - let action = Action::new("r"); - let resource = Resource::new("/foo/bar/baz"); - let store = Memory::new(); - - store.inner.write().unwrap().set_policy( - &resource, - &action, - &parse(condition).unwrap().unwrap(), - ); - - let mut e = Env::new(); - e.put("subject.age", int(25)) - .put("subject.name", str("John")) - .put("resource.version", str("1.0.0")) - .put("resource.admins", seq([str("root"), str("John")])); - - let policy = store - .inner - .write() - .unwrap() - .get_policy(&resource, &action) - .unwrap(); - assert!(eval(&policy, &e).unwrap().is_true()) - } -} diff --git a/implementations/rust/ockam/ockam_abac/src/policy.rs b/implementations/rust/ockam/ockam_abac/src/policy.rs index 90f3f6be3f0..91791047dd8 100644 --- a/implementations/rust/ockam/ockam_abac/src/policy.rs +++ b/implementations/rust/ockam/ockam_abac/src/policy.rs @@ -1,6 +1,5 @@ -use crate::traits::PolicyStorage; use crate::types::{Action, Resource}; -use crate::AbacAccessControl; +use crate::{AbacAccessControl, PoliciesRepository}; use crate::{Env, Expr}; use core::fmt; use core::fmt::{Debug, Formatter}; @@ -9,7 +8,7 @@ use ockam_core::compat::format; use ockam_core::compat::sync::Arc; use ockam_core::{async_trait, RelayMessage}; use ockam_core::{IncomingAccessControl, Result}; -use ockam_identity::IdentitiesRepository; +use ockam_identity::IdentityAttributesRepository; use tracing as log; /// Evaluates a policy expression against an environment of attributes. @@ -19,8 +18,8 @@ use tracing as log; pub struct PolicyAccessControl { resource: Resource, action: Action, - policies: Arc, - repository: Arc, + policies: Arc, + identity_attributes_repository: Arc, environment: Env, } @@ -43,8 +42,8 @@ impl PolicyAccessControl { /// the given authenticated storage, adding them the given environment, /// which may already contain other resource, action or subject attributes. pub fn new( - policies: Arc, - repository: Arc, + policies: Arc, + identity_attributes_repository: Arc, r: Resource, a: Action, env: Env, @@ -53,7 +52,7 @@ impl PolicyAccessControl { resource: r, action: a, policies, - repository, + identity_attributes_repository, environment: env, } } @@ -85,8 +84,12 @@ impl IncomingAccessControl for PolicyAccessControl { return Ok(false); }; - AbacAccessControl::new(self.repository.clone(), expr, self.environment.clone()) - .is_authorized(msg) - .await + AbacAccessControl::new( + self.identity_attributes_repository.clone(), + expr, + self.environment.clone(), + ) + .is_authorized(msg) + .await } } diff --git a/implementations/rust/ockam/ockam_abac/src/storage/lmdb_storage.rs b/implementations/rust/ockam/ockam_abac/src/storage/lmdb_storage.rs deleted file mode 100644 index fc456573519..00000000000 --- a/implementations/rust/ockam/ockam_abac/src/storage/lmdb_storage.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::tokio::task::{spawn_blocking, JoinError}; -use crate::{Action, Expr, PolicyStorage, Resource}; -use core::str; -use lmdb::{Cursor, Transaction}; -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; -use ockam_identity::storage::LmdbStorage; -use std::borrow::Cow; -use tracing as log; - -use super::PolicyEntry; - -#[async_trait] -impl PolicyStorage for LmdbStorage { - async fn get_policy(&self, r: &Resource, a: &Action) -> Result> { - let d = self.clone(); - let k = format!("{r}:{a}"); - let t = move || { - let r = d.env.begin_ro_txn().map_err(map_lmdb_err)?; - match r.get(d.map, &k) { - Ok(value) => { - let e: PolicyEntry = minicbor::decode(value)?; - Ok(Some(e.expr.into_owned())) - } - Err(lmdb::Error::NotFound) => Ok(None), - Err(e) => Err(map_lmdb_err(e)), - } - }; - spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn set_policy(&self, r: &Resource, a: &Action, c: &Expr) -> Result<()> { - let v = minicbor::to_vec(PolicyEntry { - expr: Cow::Borrowed(c), - })?; - self.write(format!("{r}:{a}"), v).await - } - - async fn del_policy(&self, r: &Resource, a: &Action) -> Result<()> { - self.delete(format!("{r}:{a}")).await - } - - async fn policies(&self, r: &Resource) -> Result> { - let d = self.clone(); - let r = r.clone(); - let t = move || { - let tx = d.env.begin_ro_txn().map_err(map_lmdb_err)?; - let mut c = tx.open_ro_cursor(d.map).map_err(map_lmdb_err)?; - let mut xs = Vec::new(); - for entry in c.iter_from(r.as_str()) { - let (k, v) = entry.map_err(map_lmdb_err)?; - let ks = str::from_utf8(k).map_err(from_utf8_err)?; - if let Some((prefix, a)) = ks.split_once(':') { - if prefix != r.as_str() { - break; - } - let x: PolicyEntry = minicbor::decode(v)?; - xs.push((Action::new(a), x.expr.into_owned())) - } else { - log::warn!(key = %ks, "malformed key in policy database") - } - } - Ok(xs) - }; - spawn_blocking(t).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_lmdb_err(err: lmdb::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn from_utf8_err(err: str::Utf8Error) -> Error { - Error::new(Origin::Other, Kind::Invalid, err) -} diff --git a/implementations/rust/ockam/ockam_abac/src/storage/mod.rs b/implementations/rust/ockam/ockam_abac/src/storage/mod.rs index aed266986cd..e732cce2680 100644 --- a/implementations/rust/ockam/ockam_abac/src/storage/mod.rs +++ b/implementations/rust/ockam/ockam_abac/src/storage/mod.rs @@ -1,26 +1,7 @@ +mod policy_repository; #[cfg(feature = "std")] -pub mod lmdb_storage; +pub mod policy_repository_sql; +pub use policy_repository::*; #[cfg(feature = "std")] -pub use lmdb_storage::*; - -#[cfg(feature = "sqlite")] -pub mod sqlite_storage; - -#[cfg(feature = "sqlite")] -pub use sqlite_storage::*; - -use minicbor::{Decode, Encode}; -use ockam_core::compat::borrow::Cow; - -use crate::Expr; - -/// Policy storage entry. -/// -/// Used instead of storing plain `Expr` values to allow for additional -/// metadata, versioning, etc. -#[derive(Debug, Encode, Decode)] -#[rustfmt::skip] -struct PolicyEntry<'a> { - #[b(0)] expr: Cow<'a, Expr>, -} +pub use policy_repository_sql::*; diff --git a/implementations/rust/ockam/ockam_abac/src/storage/policy_repository.rs b/implementations/rust/ockam/ockam_abac/src/storage/policy_repository.rs new file mode 100644 index 00000000000..9d2c7e3a604 --- /dev/null +++ b/implementations/rust/ockam/ockam_abac/src/storage/policy_repository.rs @@ -0,0 +1,23 @@ +use crate::{Action, Expr, Resource}; +use ockam_core::async_trait; +use ockam_core::compat::boxed::Box; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; + +/// This repository stores policies. +/// A policy is an expression which can be evaluated against an environment (a list of attribute +/// names and values) in order to determine if a given action can be performed on a given resource. +#[async_trait] +pub trait PoliciesRepository: Send + Sync + 'static { + /// Return the policy associated to a given resource and action + async fn get_policy(&self, r: &Resource, a: &Action) -> Result>; + + /// Set a policy for a given resource and action + async fn set_policy(&self, r: &Resource, a: &Action, c: &Expr) -> Result<()>; + + /// Delete the policy associated to a given resource and action + async fn delete_policy(&self, r: &Resource, a: &Action) -> Result<()>; + + /// Return the list of all the policies associated to a given resource + async fn get_policies_by_resource(&self, r: &Resource) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_abac/src/storage/policy_repository_sql.rs b/implementations/rust/ockam/ockam_abac/src/storage/policy_repository_sql.rs new file mode 100644 index 00000000000..ca8e11751b7 --- /dev/null +++ b/implementations/rust/ockam/ockam_abac/src/storage/policy_repository_sql.rs @@ -0,0 +1,151 @@ +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::{Action, Expr, PoliciesRepository, Resource}; + +#[derive(Clone)] +pub struct PolicySqlxDatabase { + database: Arc, +} + +impl PolicySqlxDatabase { + /// Create a new database for policies keys + pub fn new(database: Arc) -> Self { + debug!("create a repository for policies"); + Self { database } + } + + /// Create a new in-memory database for policies + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("policies").await?, + ))) + } +} + +#[async_trait] +impl PoliciesRepository for PolicySqlxDatabase { + async fn get_policy(&self, resource: &Resource, action: &Action) -> Result> { + let query = query_as("SELECT * FROM policy WHERE resource=$1 and action=$2") + .bind(resource.to_sql()) + .bind(action.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.expression()).transpose()?) + } + + async fn set_policy( + &self, + resource: &Resource, + action: &Action, + expression: &Expr, + ) -> Result<()> { + let query = query("INSERT OR REPLACE INTO policy VALUES (?, ?, ?)") + .bind(resource.to_sql()) + .bind(action.to_sql()) + .bind(minicbor::to_vec(expression)?.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_policy(&self, resource: &Resource, action: &Action) -> Result<()> { + let query = query("DELETE FROM policy WHERE resource = ? and action = ?") + .bind(resource.to_sql()) + .bind(action.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_policies_by_resource(&self, resource: &Resource) -> Result> { + let query = query_as("SELECT * FROM policy where resource = $1").bind(resource.to_sql()); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + row.into_iter() + .map(|r| r.expression().map(|e| (r.action(), e))) + .collect::>>() + } +} + +// Database serialization / deserialization + +impl ToSqlxType for Resource { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.as_str().to_string()) + } +} + +impl ToSqlxType for Action { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.as_str().to_string()) + } +} + +/// Low-level representation of a row in the policies table +#[derive(FromRow)] +pub(crate) struct PolicyRow { + resource: String, + action: String, + expression: Vec, +} + +impl PolicyRow { + #[allow(dead_code)] + pub(crate) fn resource(&self) -> Resource { + Resource::from(self.resource.clone()) + } + + pub(crate) fn action(&self) -> Action { + Action::from(self.action.clone()) + } + + pub(crate) fn expression(&self) -> Result { + Ok(minicbor::decode(self.expression.as_slice())?) + } +} + +#[cfg(test)] +mod test { + use crate::expr::*; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let repository = create_repository().await?; + + // a policy can be associated to a resource and an action + let r = Resource::from("outlet"); + let a = Action::from("create"); + let e = eq([ident("name"), str("me")]); + repository.set_policy(&r, &a, &e).await?; + assert!(repository.get_policy(&r, &a).await?.unwrap().equals(&e)?); + + // we can retrieve all the policies associated to a given resource + let policies = repository.get_policies_by_resource(&r).await?; + assert_eq!(policies.len(), 1); + + let a = Action::from("delete"); + repository.set_policy(&r, &a, &e).await?; + let policies = repository.get_policies_by_resource(&r).await?; + assert_eq!(policies.len(), 2); + + // we can delete a given policy + // here we delete the policy for outlet/delete + repository.delete_policy(&r, &a).await?; + let policies = repository.get_policies_by_resource(&r).await?; + assert_eq!(policies.len(), 1); + assert_eq!(policies.first().unwrap().0, Action::from("create")); + + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(PolicySqlxDatabase::create().await?) + } +} diff --git a/implementations/rust/ockam/ockam_abac/src/storage/sqlite_storage.rs b/implementations/rust/ockam/ockam_abac/src/storage/sqlite_storage.rs deleted file mode 100644 index b28037d0b9a..00000000000 --- a/implementations/rust/ockam/ockam_abac/src/storage/sqlite_storage.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::tokio::task::{spawn_blocking, JoinError}; -use crate::{Action, Expr, PolicyStorage, Resource}; -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; -use ockam_identity::SqliteStorage; -use rusqlite::{params, ToSql}; -use std::borrow::Cow; - -use super::PolicyEntry; - -impl ToSql for Resource { - fn to_sql(&self) -> rusqlite::Result> { - self.as_str().to_sql() - } -} - -impl ToSql for Action { - fn to_sql(&self) -> rusqlite::Result> { - self.as_str().to_sql() - } -} - -#[async_trait] -impl PolicyStorage for SqliteStorage { - async fn get_policy(&self, r: &Resource, a: &Action) -> Result> { - let conn = self.conn(); - let r = r.clone(); - let a = a.clone(); - let t = move || { - let conn = conn.lock().unwrap(); - let result = conn - .query_row::, _, _>( - "SELECT value FROM policy WHERE resource = ?1 AND action = ?2;", - params![r, a], - |row| { - row.get::<_, Vec>(0).map(|value| { - let e: PolicyEntry = minicbor::decode(&value).unwrap(); - Some(e.expr.into_owned()) - }) - }, - ) - .map_err(map_sqlite_err); - result - }; - spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn set_policy(&self, r: &Resource, a: &Action, c: &Expr) -> Result<()> { - let conn = self.conn(); - let r = r.clone(); - let a = a.clone(); - let v = minicbor::to_vec(PolicyEntry { - expr: Cow::Borrowed(c), - })?; - let t = move || { - let conn = conn.lock().unwrap(); - conn.execute( - "INSERT OR REPLACE INTO policy (resource, action, value) VALUES (?1, ?2, ?3)", - params![r, a, v], - ) - .map_err(map_sqlite_err)?; - Ok(()) - }; - spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn del_policy(&self, r: &Resource, a: &Action) -> Result<()> { - let conn = self.conn(); - let r = r.clone(); - let a = a.clone(); - let t = move || { - let conn = conn.lock().unwrap(); - conn.execute( - "DELETE FROM policy WHERE resource = ?1 AND action = ?2;", - params![r, a], - ) - .map_err(map_sqlite_err)?; - Ok(()) - }; - spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn policies(&self, r: &Resource) -> Result> { - let conn = self.conn(); - let r = r.clone(); - let t = move || { - let conn = conn.lock().unwrap(); - let mut stmt = conn - .prepare("SELECT action, value FROM policy WHERE resource = ?1;") - .map_err(map_sqlite_err)?; - let result = stmt - .query_map::<(Action, Vec), _, _>(params![r], |row| { - let action: Action = Action::from(row.get::<_, String>(0)?); - let value: Vec = row.get(1)?; - Ok((action, value)) - }) - .map_err(map_sqlite_err)? - .map( - |value: core::result::Result<(Action, Vec), rusqlite::Error>| { - value.map_err(map_sqlite_err) - }, - ) - .collect::)>, Error>>()?; - let decoded_result = result - .iter() - .map(|(action, value)| { - let e: PolicyEntry = minicbor::decode(value).map_err(map_decode_err)?; - Ok((action.to_owned(), e.expr.into_owned())) - }) - .collect(); - decoded_result - }; - spawn_blocking(t).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_sqlite_err(err: rusqlite::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_decode_err(err: minicbor::decode::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -#[cfg(test)] -mod test { - use super::*; - use core::str::FromStr; - use tempfile::NamedTempFile; - - #[tokio::test] - async fn test_basic_functionality() -> Result<()> { - let temp_path = NamedTempFile::new().unwrap().into_temp_path(); - let db = SqliteStorage::new(temp_path.to_path_buf()).await?; - - let r = Resource::from("1"); - let a = Action::from("2"); - let e = Expr::from_str("345")?; - db.set_policy(&r, &a, &e).await?; - assert!( - db.get_policy(&r, &a).await?.unwrap().equals(&e)?, - "Verify set and get" - ); - - let policies = db.policies(&r).await?; - assert_eq!(policies.len(), 1); - - let a = Action::from("3"); - db.set_policy(&r, &a, &e).await?; - let policies = db.policies(&r).await?; - assert_eq!(policies.len(), 2); - - db.del_policy(&r, &a).await?; - let policies = db.policies(&r).await?; - assert_eq!(policies.len(), 1); - - Ok(()) - } -} diff --git a/implementations/rust/ockam/ockam_abac/src/traits.rs b/implementations/rust/ockam/ockam_abac/src/traits.rs deleted file mode 100644 index 9a9509c7ac8..00000000000 --- a/implementations/rust/ockam/ockam_abac/src/traits.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::{Action, Expr, Resource}; -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::vec::Vec; -use ockam_core::Result; - -#[async_trait] -pub trait PolicyStorage: Send + Sync + 'static { - async fn get_policy(&self, r: &Resource, a: &Action) -> Result>; - async fn set_policy(&self, r: &Resource, a: &Action, c: &Expr) -> Result<()>; - async fn del_policy(&self, r: &Resource, a: &Action) -> Result<()>; - async fn policies(&self, r: &Resource) -> Result>; -} diff --git a/implementations/rust/ockam/ockam_api/Cargo.toml b/implementations/rust/ockam/ockam_api/Cargo.toml index a8ce52273f7..a85284db34a 100644 --- a/implementations/rust/ockam/ockam_api/Cargo.toml +++ b/implementations/rust/ockam/ockam_api/Cargo.toml @@ -15,8 +15,8 @@ std = [ "either/use_std", "hex/std", "minicbor/std", - "ockam_core/std", "ockam_abac/std", + "ockam_core/std", "ockam/std", "ockam_multiaddr/std", "ockam_node/std", @@ -24,8 +24,9 @@ std = [ "ockam_vault_aws/std", "tinyvec/std", "tracing/std", + "storage", ] -vault-storage = ["ockam_vault/storage"] +storage = ["ockam/storage"] [dependencies] anyhow = "1" @@ -34,6 +35,7 @@ base64-url = "2.0.0" bytes = { version = "1.5.0", default-features = false, features = ["serde"] } either = { version = "1.9.0", default-features = false } fs2 = { version = "0.4.3" } +futures = { version = "0.3.28" } hex = { version = "0.4.3", default-features = false, features = ["alloc", "serde"] } home = "0.5" kafka-protocol = "0.7.0" @@ -46,6 +48,7 @@ rand = "0.8" reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls-native-roots"] } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" +sqlx = { version = "0.7.3", features = ["runtime-tokio", "sqlite"] } sysinfo = "0.29" tempfile = "3.8.0" thiserror = "1.0" @@ -90,7 +93,7 @@ features = ["std"] version = "^0.105.0" path = "../ockam" default-features = false -features = ["ockam_transport_tcp", "software_vault_storage"] +features = ["ockam_transport_tcp", "storage"] [dependencies.ockam_abac] version = "0.39.0" diff --git a/implementations/rust/ockam/ockam_api/README.md b/implementations/rust/ockam/ockam_api/README.md index a7a639eeef7..f1ea50ff268 100644 --- a/implementations/rust/ockam/ockam_api/README.md +++ b/implementations/rust/ockam/ockam_api/README.md @@ -13,126 +13,18 @@ This crate supports the creation of a fully-featured Ockam Node ## Configuration -A `NodeManager` maintains its configuration as a list of directories and files stored under +A `NodeManager` maintains its database and log files on disk in the `OCKAM_HOME` directory (`~/.ockam`) by default: ```shell root -├─ credentials -│ ├─ c1.json -│ ├─ c2.json -│ └─ ... -├─ defaults -│ ├── credential -> ... -│ ├── identity -> ... -│ ├── node -> ... -│ └── vault -> ... -├─ identities -│ ├─ data -│ │ ├─ authenticated-storage.lmdb -│ │ └─ authenticated-storage.lmdb-lock -│ ├─ identity1.json -│ ├─ identity2.json -│ └─ ... +├─ database.sqlite ├─ nodes │ ├─ node1 -│ │ ├─ default_identity -> ... -│ │ ├─ default_vault -> ... -│ │ ├─ policies-storage.lmdb -│ │ ├─ policies-storage.lmdb-lock -│ │ ├─ setup.json │ │ ├─ stderr.log │ │ ├─ stdout.log -│ │ └─ version.log │ ├─ node2 │ └─ ... -├─ projects -│ └─ default.json -├─ trust_contexts -│ └─ default.json -└─ vaults - ├─ vault1.json - ├─ vault2.json - ├─ ... - └─ data - ├─ vault1.lmdb - ├─ vault1.lmdb-lock - ├─ vault2.lmdb - ├─ vault2.lmdb-lock - └─ ... ``` -## `credentials` - -Each file stored under the `credentials` directory contains the credential for a given identity. -Those files are created with the `ockam credential store` command. They are then read during the creation of -a secure channel to send the credentials to the other party - -## `defaults` - -This directory contains symlinks to other files or directories in order to specify which node, -identity, credential or vault must be considered as a default when running a command expecting those -inputs - -## `identities` - -This directory contains one file per identity and a data directory. An identity file is created -with the `ockam identity create` command or created by default for some commands (in that case the -`defaults/identity` symlink points to that identity). The identity file contains: - -- the identity identifier -- the enrollment status for that identity - -The `data` directory contains a LMDB database with other information about identities: - - the credential attributes that have been verified for this identity. Those attributes are - generally used in ABAC rules that are specified on secure channels. For example when sending messages - via a secure channel and using the Orchestrator the `project` attribute will be checked and the LMDB database accessed - - - the list of key changes for each identity. These key changes are created (or updated) when an identity - is created either by using the command line or by using the identity service. - The key changes are accessed in order to get the latest public key associated to a given identity - when checking its signature during the creation of a secure channel. - They are also accessed to retrieve the key id associated to that key and then use a Vault to create a signature - for an identity - -Note: for each `.lmdb` file there is a corresponding `lmdb-lock` file which is used to control -the exclusive access to the LMDB database even if several OS processes are trying to modify it. -For example when several nodes are started using the same `NodeManager`. - -## `nodes` - -This directory contains: - - - symlinks to default values for the node: identity and vault - - a database for ABAC policies - - a setup file containing some configuration information for the node (is it an authority node?, what is the TCP listener address?,...). - That file is created when a node is created and read again if the node is restarted - - log files: for system errors and system outputs. The stdout.log file is where almost all the node logs are written - - a version number for the configuration - -## `projects` - -This directory contains a list of files, one per project that was created, either the default project -or via the `ockam project create` command. A project file contains: - - - the project identifier and the space it belongs to - - the authority used by that project (identity, route) - - the configuration for the project plugins - -## `trust_context` - -This directory contains a list of files, one per trust context. A trust context can created with -the `ockam trust_context create` command. It can then be referred to during the creation of a -secure channel as a way to specify which authority can attest to the validity of which attributes - -## `vaults` - -This directory contains one file per vault that is either created by default or with the `ockam vault create` -command. That file contains the configuration for the vault, which for now consists only in -declaring if the vault is backed by an AWS KMS or not. - -The rest of the vault data is stored in an LMDB database under the `data` directory with one `.lmdb` -file per vault. A vault contains secrets which are generally used during the creation of secure -channels to sign or encrypt data involved in the handshake. - ## Usage diff --git a/implementations/rust/ockam/ockam_api/src/auth.rs b/implementations/rust/ockam/ockam_api/src/auth.rs index b1eb1e37828..01b5530a2d4 100644 --- a/implementations/rust/ockam/ockam_api/src/auth.rs +++ b/implementations/rust/ockam/ockam_api/src/auth.rs @@ -4,7 +4,7 @@ use minicbor::Decoder; use tracing::trace; use crate::nodes::BackgroundNode; -use ockam::identity::{AttributesEntry, Identifier, IdentityAttributesReader}; +use ockam::identity::{AttributesEntry, Identifier, IdentityAttributesRepository}; use ockam_core::api::{Method, RequestHeader}; use ockam_core::api::{Request, Response}; use ockam_core::compat::sync::Arc; @@ -16,7 +16,7 @@ pub mod types; /// Auth API server. pub struct Server { - store: Arc, + identity_attributes_repository: Arc, } #[ockam_core::worker] @@ -35,8 +35,10 @@ impl Worker for Server { } impl Server { - pub fn new(s: Arc) -> Self { - Server { store: s } + pub fn new(identity_attributes_repository: Arc) -> Self { + Server { + identity_attributes_repository, + } } async fn on_request(&mut self, data: &[u8]) -> Result> { @@ -54,10 +56,20 @@ impl Server { let res = match req.method() { Some(Method::Get) => match req.path_segments::<2>().as_slice() { - [""] => Response::ok(&req).body(self.store.list().await?).to_vec()?, + [""] => Response::ok(&req) + .body( + self.identity_attributes_repository + .list_attributes_by_identifier() + .await?, + ) + .to_vec()?, [id] => { let identifier = Identifier::try_from(id.to_string())?; - if let Some(a) = self.store.get_attributes(&identifier).await? { + if let Some(a) = self + .identity_attributes_repository + .get_attributes(&identifier) + .await? + { Response::ok(&req).body(a).to_vec()? } else { Response::not_found(&req, &format!("identity {} not found", id)).to_vec()? diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/direct/authenticator.rs b/implementations/rust/ockam/ockam_api/src/authenticator/direct/authenticator.rs index d3e815efbb2..af2f5943ebd 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/direct/authenticator.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/direct/authenticator.rs @@ -1,33 +1,32 @@ +use std::collections::HashMap; + use minicbor::Decoder; +use tracing::trace; + use ockam::identity::utils::now; -use ockam::identity::{secure_channel_required, TRUST_CONTEXT_ID}; -use ockam::identity::{AttributesEntry, IdentityAttributesReader, IdentityAttributesWriter}; +use ockam::identity::AttributesEntry; +use ockam::identity::{secure_channel_required, IdentityAttributesRepository, TRUST_CONTEXT_ID}; use ockam::identity::{Identifier, IdentitySecureChannelLocalInfo}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::sync::Arc; use ockam_core::{CowStr, Result, Routed, Worker}; use ockam_node::Context; -use std::collections::HashMap; -use tracing::trace; use crate::authenticator::direct::types::AddMember; pub struct DirectAuthenticator { trust_context: String, - attributes_writer: Arc, - attributes_reader: Arc, + identity_attributes_repository: Arc, } impl DirectAuthenticator { pub async fn new( trust_context: String, - attributes_writer: Arc, - attributes_reader: Arc, + identity_attributes_repository: Arc, ) -> Result { Ok(Self { trust_context, - attributes_writer, - attributes_reader, + identity_attributes_repository, }) } @@ -49,11 +48,16 @@ impl DirectAuthenticator { ) .collect(); let entry = AttributesEntry::new(auth_attrs, now()?, None, Some(enroller.clone())); - self.attributes_writer.put_attributes(id, entry).await + self.identity_attributes_repository + .put_attributes(id, entry) + .await } async fn list_members(&self) -> Result> { - let all_attributes = self.attributes_reader.list().await?; + let all_attributes = self + .identity_attributes_repository + .list_attributes_by_identifier() + .await?; let attested_by_me = all_attributes.into_iter().collect(); Ok(attested_by_me) } @@ -98,7 +102,9 @@ impl Worker for DirectAuthenticator { } (Some(Method::Delete), [id]) | (Some(Method::Delete), ["members", id]) => { let identifier = Identifier::try_from(id.to_string())?; - self.attributes_writer.delete(&identifier).await?; + self.identity_attributes_repository + .delete(&identifier) + .await?; Response::ok(&req).to_vec()? } diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs index a4c756e7435..1caf3c6c93f 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs @@ -2,7 +2,7 @@ use minicbor::Decoder; use ockam::identity::utils::now; use ockam::identity::OneTimeCode; use ockam::identity::{secure_channel_required, TRUST_CONTEXT_ID}; -use ockam::identity::{AttributesEntry, IdentityAttributesWriter}; +use ockam::identity::{AttributesEntry, IdentityAttributesRepository}; use ockam::identity::{Identifier, IdentitySecureChannelLocalInfo}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::sync::Arc; @@ -14,7 +14,7 @@ use crate::authenticator::enrollment_tokens::EnrollmentTokenAuthenticator; pub struct EnrollmentTokenAcceptor( pub(super) EnrollmentTokenAuthenticator, - pub(super) Arc, + pub(super) Arc, ); impl EnrollmentTokenAcceptor { diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/authenticator.rs b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/authenticator.rs index ba3418f2f32..1693f07d1b3 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/authenticator.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/authenticator.rs @@ -1,4 +1,4 @@ -use ockam::identity::IdentityAttributesWriter; +use ockam::identity::IdentityAttributesRepository; use ockam_core::compat::collections::HashMap; use ockam_core::compat::sync::{Arc, RwLock}; use std::time::Duration; @@ -18,7 +18,7 @@ pub struct EnrollmentTokenAuthenticator { impl EnrollmentTokenAuthenticator { pub fn new_worker_pair( trust_context: String, - attributes_writer: Arc, + identity_attributes_repository: Arc, ) -> (EnrollmentTokenIssuer, EnrollmentTokenAcceptor) { let base = Self { trust_context, @@ -26,7 +26,7 @@ impl EnrollmentTokenAuthenticator { }; ( EnrollmentTokenIssuer(base.clone()), - EnrollmentTokenAcceptor(base, attributes_writer), + EnrollmentTokenAcceptor(base, identity_attributes_repository), ) } } diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer.rs b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer.rs index 4088b1102e0..e07b66ffeac 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer.rs @@ -1,5 +1,10 @@ +use std::collections::HashMap; +use std::time::{Duration, Instant}; + use miette::IntoDiagnostic; use minicbor::Decoder; +use tracing::trace; + use ockam::identity::OneTimeCode; use ockam::identity::{secure_channel_required, AttributesEntry}; use ockam::identity::{Identifier, IdentitySecureChannelLocalInfo}; @@ -7,16 +12,13 @@ use ockam_core::api::{Method, Request, RequestHeader, Response}; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{async_trait, Result, Routed, Worker}; use ockam_node::Context; -use std::collections::HashMap; -use std::time::{Duration, Instant}; -use tracing::trace; use crate::authenticator::direct::types::{AddMember, CreateToken}; use crate::authenticator::enrollment_tokens::authenticator::MAX_TOKEN_DURATION; use crate::authenticator::enrollment_tokens::types::Token; use crate::authenticator::enrollment_tokens::EnrollmentTokenAuthenticator; use crate::cloud::AuthorityNode; -use crate::DefaultAddress; +use crate::nodes::service::default_address::DefaultAddress; pub struct EnrollmentTokenIssuer(pub(super) EnrollmentTokenAuthenticator); diff --git a/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs b/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs index 79a0d941189..1dd3a4a85a9 100644 --- a/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs +++ b/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs @@ -2,12 +2,12 @@ use std::path::Path; use tracing::info; -use ockam::identity::storage::LmdbStorage; -use ockam::identity::Vault; +use ockam::identity::storage::PurposeKeysSqlxDatabase; +use ockam::identity::{ChangeHistorySqlxDatabase, Vault}; use ockam::identity::{ - CredentialsIssuer, Identifier, Identities, IdentitiesRepository, IdentitiesStorage, - IdentityAttributesReader, IdentityAttributesWriter, SecureChannelListenerOptions, - SecureChannels, TrustEveryonePolicy, + CredentialsIssuer, Identifier, Identities, IdentityAttributesRepository, + IdentityAttributesSqlxDatabase, SecureChannelListenerOptions, SecureChannels, + TrustEveryonePolicy, }; use ockam_abac::expr::{and, eq, ident, str}; use ockam_abac::{AbacAccessControl, Env}; @@ -15,15 +15,17 @@ use ockam_core::compat::sync::Arc; use ockam_core::errcode::{Kind, Origin}; use ockam_core::flow_control::FlowControlId; use ockam_core::{Error, Result, Worker}; +use ockam_node::database::SqlxDatabase; use ockam_node::{Context, WorkerBuilder}; use ockam_transport_tcp::{TcpListenerOptions, TcpTransport}; use crate::authenticator::enrollment_tokens::EnrollmentTokenAuthenticator; use crate::authority_node::authority::EnrollerCheck::{AnyMember, EnrollerOnly}; use crate::authority_node::Configuration; -use crate::bootstrapped_identities_store::BootstrapedIdentityStore; +use crate::bootstrapped_identities_store::BootstrapedIdentityAttributesStore; use crate::echoer::Echoer; -use crate::{actions, DefaultAddress}; +use crate::nodes::service::actions; +use crate::nodes::service::default_address::DefaultAddress; /// This struct represents an Authority, which is an /// Identity which other identities trust to authenticate attributes @@ -56,11 +58,27 @@ impl Authority { /// In practice it contains the list of identities with the ockam-role attribute set as 'enroller' pub async fn create(configuration: &Configuration) -> Result { debug!(?configuration, "creating the authority"); - let vault = Self::create_secure_channels_vault(configuration).await?; - let repository = Self::create_identities_repository(configuration).await?; + + // create the database + let database_path = &configuration.database_path; + Self::create_ockam_directory_if_necessary(database_path)?; + let database = Arc::new(SqlxDatabase::create(database_path).await?); + + // create the repositories + let vault = Vault::create_with_database(database.clone()); + let identity_attributes_repository = + Arc::new(IdentityAttributesSqlxDatabase::new(database.clone())); + let identity_attributes_repository = + Self::bootstrap_repository(identity_attributes_repository, configuration); + let change_history_repository = Arc::new(ChangeHistorySqlxDatabase::new(database.clone())); + let purpose_keys_repository = Arc::new(PurposeKeysSqlxDatabase::new(database)); + let secure_channels = SecureChannels::builder() + .await? .with_vault(vault) - .with_identities_repository(repository) + .with_identity_attributes_repository(identity_attributes_repository) + .with_change_history_repository(change_history_repository) + .with_purpose_keys_repository(purpose_keys_repository) .build(); let identifier = configuration.identifier(); @@ -100,7 +118,10 @@ impl Authority { let tcp = TcpTransport::create(ctx).await?; let listener = tcp - .listen(configuration.tcp_listener_address(), tcp_listener_options) + .listen( + configuration.tcp_listener_address().to_string(), + tcp_listener_options, + ) .await?; info!("started a TCP listener at {listener:?}"); @@ -120,8 +141,7 @@ impl Authority { let direct = crate::authenticator::direct::DirectAuthenticator::new( configuration.project_identifier(), - self.attributes_writer(), - self.attributes_reader(), + self.identity_attributes_repository(), ) .await?; @@ -149,7 +169,7 @@ impl Authority { let (issuer, acceptor) = EnrollmentTokenAuthenticator::new_worker_pair( configuration.project_identifier(), - self.attributes_writer(), + self.identity_attributes_repository(), ); // start an enrollment token issuer with an abac policy checking that @@ -192,7 +212,9 @@ impl Authority { ) -> Result<()> { // create and start a credential issuer worker let issuer = CredentialsIssuer::new( - self.secure_channels.identities().repository(), + self.secure_channels + .identities() + .identity_attributes_repository(), self.secure_channels.identities().credentials(), &self.identifier, configuration.project_identifier(), @@ -218,7 +240,7 @@ impl Authority { ) -> Result<()> { if let Some(okta) = &configuration.okta { let okta_worker = crate::okta::Server::new( - self.attributes_writer(), + self.identity_attributes_repository(), configuration.project_identifier(), okta.tenant_base_url(), okta.certificate(), @@ -255,38 +277,9 @@ impl Authority { self.secure_channels.identities() } - /// Return the identities repository used by the authority - fn identities_repository(&self) -> Arc { - self.identities().repository().clone() - } - - /// Return the identities repository as writer used by the authority - fn attributes_writer(&self) -> Arc { - self.identities_repository().as_attributes_writer().clone() - } - - /// Return the identities repository as reader used by the authority - fn attributes_reader(&self) -> Arc { - self.identities_repository().as_attributes_reader().clone() - } - - /// Create an identity vault backed by a FileStorage - async fn create_secure_channels_vault(configuration: &Configuration) -> Result { - let vault_path = &configuration.vault_path; - Self::create_ockam_directory_if_necessary(vault_path)?; - let vault = Vault::create_with_persistent_storage_path(vault_path).await?; - Ok(vault) - } - - /// Create an authenticated storage backed by a Lmdb database - async fn create_identities_repository( - configuration: &Configuration, - ) -> Result> { - let storage_path = &configuration.storage_path; - Self::create_ockam_directory_if_necessary(storage_path)?; - let storage = Arc::new(LmdbStorage::new(&storage_path).await?); - let repository = Arc::new(IdentitiesStorage::new(storage)); - Ok(Self::bootstrap_repository(repository, configuration)) + /// Return the identity attributes repository used by the authority + fn identity_attributes_repository(&self) -> Arc { + self.identities().identity_attributes_repository().clone() } /// Create a directory to save storage files if they haven't been created before @@ -302,11 +295,11 @@ impl Authority { /// identities. The values either come from the command line or are read directly from a file /// every time we try to retrieve some attributes fn bootstrap_repository( - repository: Arc, + repository: Arc, configuration: &Configuration, - ) -> Arc { + ) -> Arc { let trusted_identities = &configuration.trusted_identities; - Arc::new(BootstrapedIdentityStore::new( + Arc::new(BootstrapedIdentityAttributesStore::new( Arc::new(trusted_identities.clone()), repository.clone(), )) @@ -369,7 +362,7 @@ impl Authority { str(configuration.project_identifier.clone()), ); let abac = Arc::new(AbacAccessControl::new( - self.identities_repository(), + self.identity_attributes_repository(), rule, env, )); diff --git a/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs b/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs index 16f8487864a..3e26f038369 100644 --- a/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs +++ b/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs @@ -1,14 +1,17 @@ -use crate::bootstrapped_identities_store::PreTrustedIdentities; -use crate::DefaultAddress; +use std::collections::BTreeMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; use ockam::identity::utils::now; use ockam::identity::{AttributesEntry, Identifier, TRUST_CONTEXT_ID}; use ockam_core::compat::collections::HashMap; use ockam_core::compat::fmt; use ockam_core::compat::fmt::{Display, Formatter}; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::path::PathBuf; + +use crate::bootstrapped_identities_store::PreTrustedIdentities; +use crate::config::lookup::InternetAddress; +use crate::nodes::service::default_address::DefaultAddress; /// Configuration for the Authority node #[derive(Debug, Clone, Serialize, Deserialize)] @@ -16,17 +19,14 @@ pub struct Configuration { /// Authority identity or identity associated with the newly created node pub identifier: Identifier, - /// path where the storage for identity attributes should be persisted - pub storage_path: PathBuf, - - /// path where secrets should be persisted - pub vault_path: PathBuf, + /// path where the database should be stored + pub database_path: PathBuf, /// Project identifier on the Orchestrator node pub project_identifier: String, /// listener address for the TCP listener, for example "127.0.0.1:4000" - pub tcp_listener_address: String, + pub tcp_listener_address: InternetAddress, /// service name for the secure channel listener, for example "secure" /// The default is DefaultAddress::SECURE_CHANNEL_LISTENER @@ -62,7 +62,7 @@ impl Configuration { } /// Return the address for the TCP listener - pub(crate) fn tcp_listener_address(&self) -> String { + pub(crate) fn tcp_listener_address(&self) -> InternetAddress { self.tcp_listener_address.clone() } diff --git a/implementations/rust/ockam/ockam_api/src/bootstrapped_identities_store.rs b/implementations/rust/ockam/ockam_api/src/bootstrapped_identities_store.rs index 47de22c2a77..14b7d27be3c 100644 --- a/implementations/rust/ockam/ockam_api/src/bootstrapped_identities_store.rs +++ b/implementations/rust/ockam/ockam_api/src/bootstrapped_identities_store.rs @@ -1,29 +1,27 @@ -use ockam::identity::models::ChangeHistory; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use serde_json as json; +use tracing::trace; + use ockam::identity::utils::now; -use ockam::identity::{ - AttributesEntry, Identifier, IdentitiesReader, IdentitiesRepository, IdentitiesWriter, - IdentityAttributesReader, IdentityAttributesWriter, -}; +use ockam::identity::{AttributesEntry, Identifier, IdentityAttributesRepository}; use ockam_core::async_trait; use ockam_core::compat::sync::Arc; use ockam_core::compat::{collections::HashMap, string::String, vec::Vec}; use ockam_core::errcode::{Kind, Origin}; use ockam_core::Result; -use serde::{Deserialize, Serialize}; -use serde_json as json; -use std::path::PathBuf; -use tracing::trace; #[derive(Clone)] -pub struct BootstrapedIdentityStore { - bootstrapped: Arc, - repository: Arc, +pub struct BootstrapedIdentityAttributesStore { + bootstrapped: Arc, + repository: Arc, } -impl BootstrapedIdentityStore { +impl BootstrapedIdentityAttributesStore { pub fn new( - bootstrapped: Arc, - repository: Arc, + bootstrapped: Arc, + repository: Arc, ) -> Self { Self { bootstrapped, @@ -33,7 +31,7 @@ impl BootstrapedIdentityStore { } #[async_trait] -impl IdentityAttributesReader for BootstrapedIdentityStore { +impl IdentityAttributesRepository for BootstrapedIdentityAttributesStore { async fn get_attributes(&self, identity_id: &Identifier) -> Result> { trace! { target: "ockam_api::bootstrapped_identities_store", @@ -46,16 +44,13 @@ impl IdentityAttributesReader for BootstrapedIdentityStore { } } - async fn list(&self) -> Result> { - let mut l = self.repository.list().await?; - let mut l2 = self.bootstrapped.list().await?; + async fn list_attributes_by_identifier(&self) -> Result> { + let mut l = self.repository.list_attributes_by_identifier().await?; + let mut l2 = self.bootstrapped.list_attributes_by_identifier().await?; l.append(&mut l2); Ok(l) } -} -#[async_trait] -impl IdentityAttributesWriter for BootstrapedIdentityStore { async fn put_attributes(&self, sender: &Identifier, entry: AttributesEntry) -> Result<()> { trace! { target: "ockam_api::bootstrapped_identities_store", @@ -89,47 +84,6 @@ impl IdentityAttributesWriter for BootstrapedIdentityStore { } } -#[async_trait] -impl IdentitiesReader for BootstrapedIdentityStore { - async fn retrieve_identity(&self, identifier: &Identifier) -> Result> { - self.repository.retrieve_identity(identifier).await - } - async fn get_identity(&self, identifier: &Identifier) -> Result { - self.repository.get_identity(identifier).await - } -} - -#[async_trait] -impl IdentitiesWriter for BootstrapedIdentityStore { - async fn update_identity( - &self, - identifier: &Identifier, - change_history: &ChangeHistory, - ) -> Result<()> { - self.repository - .update_identity(identifier, change_history) - .await - } -} - -impl IdentitiesRepository for BootstrapedIdentityStore { - fn as_attributes_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_attributes_writer(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_identities_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_identities_writer(&self) -> Arc { - Arc::new(self.clone()) - } -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub enum PreTrustedIdentities { Fixed(HashMap), @@ -183,7 +137,7 @@ impl From> for PreTrustedIdentities { } #[async_trait] -impl IdentityAttributesReader for PreTrustedIdentities { +impl IdentityAttributesRepository for PreTrustedIdentities { async fn get_attributes(&self, identity_id: &Identifier) -> Result> { match self { PreTrustedIdentities::Fixed(trusted) => Ok(trusted.get(identity_id).cloned()), @@ -193,7 +147,7 @@ impl IdentityAttributesReader for PreTrustedIdentities { } } - async fn list(&self) -> Result> { + async fn list_attributes_by_identifier(&self) -> Result> { match self { PreTrustedIdentities::Fixed(trusted) => Ok(trusted .into_iter() @@ -205,4 +159,21 @@ impl IdentityAttributesReader for PreTrustedIdentities { .collect()), } } + + async fn put_attributes(&self, _identity: &Identifier, _entry: AttributesEntry) -> Result<()> { + Ok(()) + } + + async fn put_attribute_value( + &self, + _subject: &Identifier, + _attribute_name: Vec, + _attribute_value: Vec, + ) -> Result<()> { + Ok(()) + } + + async fn delete(&self, _identity: &Identifier) -> Result<()> { + Ok(()) + } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs b/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs new file mode 100644 index 00000000000..ab8238153db --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs @@ -0,0 +1,175 @@ +use std::path::{Path, PathBuf}; + +use rand::random; + +use cli_state::error::Result; +use ockam::SqlxDatabase; +use ockam_core::compat::sync::Arc; +use ockam_core::env::get_env_with_default; +use ockam_node::Executor; + +use crate::cli_state; +use crate::cli_state::CliStateError; + +/// The CliState struct manages all the data persisted locally. +/// +/// The data is mostly saved to one database file (there can be additional files if distinct vaults are created) +/// accessed with the SqlxDatabase struct. +/// +/// However all the SQL queries for creating / updating / deleting entities are implemented by repositories, +/// for each data type: Project, Space, Vault, Identity, etc... +/// +/// The repositories themselves are not accessible from the `CliState` directly since it is often +/// necessary to use more than one repository to implement a given behaviour. For example deleting +/// an identity requires to query the nodes that are using that identity and only delete it if no +/// node is using that identity +/// +#[derive(Debug, Clone)] +pub struct CliState { + dir: PathBuf, + database: Arc, +} + +impl CliState { + /// Create a new CliState in a given directory + pub fn new(dir: &Path) -> Result { + Executor::execute_future(Self::create(dir.into()))? + } + + pub fn dir(&self) -> PathBuf { + self.dir.clone() + } + + pub fn database_path(&self) -> PathBuf { + Self::make_database_path(&self.dir) + } +} + +/// These functions allow to create and reset the local state +impl CliState { + /// Return a new CliState using a default directory to store its data + pub fn with_default_dir() -> Result { + Self::new(Self::default_dir()?.as_path()) + } + + /// Stop nodes and remove all the directories storing state + pub async fn reset(&self) -> Result<()> { + self.delete_all_nodes(true).await?; + self.delete_all_named_vaults().await?; + self.delete() + } + + /// Delete the local database and log files + pub fn delete(&self) -> Result<()> { + Self::delete_at(&self.dir) + } + + /// Reset all directories and return a new CliState + pub async fn recreate(&self) -> Result { + self.reset().await?; + Self::create(self.dir.clone()).await + } + + /// Backup and reset is used to save aside + /// some corrupted local state for later inspection and then reset the state + pub fn backup_and_reset() -> Result { + let dir = Self::default_dir()?; + + // Reset backup directory + let backup_dir = Self::backup_default_dir()?; + if backup_dir.exists() { + let _ = std::fs::remove_dir_all(&backup_dir); + } + std::fs::create_dir_all(&backup_dir)?; + + // Move state to backup directory + for entry in std::fs::read_dir(&dir)? { + let entry = entry?; + let from = entry.path(); + let to = backup_dir.join(entry.file_name()); + std::fs::rename(from, to)?; + } + + // Reset state + Self::delete_at(&dir)?; + let state = Self::new(&dir)?; + + let dir = &state.dir; + let backup_dir = CliState::backup_default_dir().unwrap(); + eprintln!("The {dir:?} directory has been reset and has been backed up to {backup_dir:?}"); + Ok(state) + } + + /// Returns the default backup directory for the CLI state. + pub fn backup_default_dir() -> Result { + let dir = Self::default_dir()?; + let dir_name = + dir.file_name() + .and_then(|n| n.to_str()) + .ok_or(CliStateError::InvalidOperation( + "The $OCKAM_HOME directory does not have a valid name".to_string(), + ))?; + let parent = dir.parent().ok_or(CliStateError::InvalidOperation( + "The $OCKAM_HOME directory does not a valid parent directory".to_string(), + ))?; + Ok(parent.join(format!("{dir_name}.bak"))) + } +} + +/// Low-level functions for creating / deleting CliState files +impl CliState { + /// Create a new CliState where the data is stored at a given path + pub(super) async fn create(dir: PathBuf) -> Result { + std::fs::create_dir_all(&dir)?; + let database = Arc::new(SqlxDatabase::create(Self::make_database_path(&dir)).await?); + debug!("Opened the database with options {:?}", database); + let state = Self { dir, database }; + Ok(state) + } + + pub(super) fn database(&self) -> Arc { + self.database.clone() + } + + pub(super) fn make_database_path(root_path: &Path) -> PathBuf { + root_path.join("database.sqlite3") + } + + pub(super) fn make_node_dir_path(root_path: &Path, node_name: &str) -> PathBuf { + Self::make_nodes_dir_path(root_path).join(node_name) + } + + pub(super) fn make_nodes_dir_path(root_path: &Path) -> PathBuf { + root_path.join("nodes") + } + + /// Delete the state files + fn delete_at(root_path: &Path) -> Result<()> { + // Delete nodes logs + let _ = std::fs::remove_dir_all(Self::make_nodes_dir_path(root_path)); + // Delete the database + let _ = std::fs::remove_file(Self::make_database_path(root_path)); + // If the state directory is now empty, delete it + let _ = std::fs::remove_dir(root_path); + Ok(()) + } + + /// Returns the default directory for the CLI state. + /// That directory is determined by `OCKAM_HOME` environment variable and is + /// $OCKAM_HOME/.ockam. + /// + /// If $OCKAM_HOME is not defined then $HOME is used instead + fn default_dir() -> Result { + Ok(get_env_with_default::( + "OCKAM_HOME", + home::home_dir() + .ok_or(CliStateError::InvalidPath("$HOME".to_string()))? + .join(".ockam"), + )?) + } +} + +/// Return a random, but memorable, name which can be used to name identities, nodes, vaults, etc... +pub fn random_name() -> String { + petname::petname(2, "-").unwrap_or(hex::encode(random::<[u8; 4]>())) +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/credentials.rs b/implementations/rust/ockam/ockam_api/src/cli_state/credentials.rs index 1ec52a829e4..370d3f3d35f 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/credentials.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/credentials.rs @@ -1,107 +1,175 @@ +use ockam::identity::models::{ChangeHistory, CredentialAndPurposeKey}; +use ockam::identity::{AttributesEntry, Identifier, Identity}; + +use crate::cli_state::{CliState, CliStateError}; + use super::Result; -use crate::cli_state::CliStateError; -use ockam::identity::models::CredentialAndPurposeKey; -use ockam::identity::Identifier; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct CredentialsState { - dir: PathBuf, -} -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct CredentialState { - name: String, - path: PathBuf, - config: CredentialConfig, -} +impl CliState { + /// Store a credential inside the local database + /// This function stores both the credential as a named entity + /// and the identity attributes in another table. + /// TODO: normalize the storage so that the data is only represented once + pub async fn store_credential( + &self, + name: &str, + issuer: &Identity, + credential: CredentialAndPurposeKey, + ) -> Result<()> { + // store the subject attributes + let credential_data = credential.get_credential_data()?; + let identity_attributes_repository = self.identity_attributes_repository().await?; + if let Some(subject) = credential_data.subject { + let attributes_entry = AttributesEntry::new( + credential_data + .subject_attributes + .map + .into_iter() + .map(|(k, v)| (k.to_vec(), v.to_vec())) + .collect(), + credential_data.created_at, + Some(credential_data.expires_at), + Some(issuer.identifier().clone()), + ); + identity_attributes_repository + .put_attributes(&subject, attributes_entry) + .await?; + } + + // store the credential itself + let credentials_repository = self.credentials_repository().await?; + credentials_repository + .store_credential(name, issuer, credential) + .await?; + Ok(()) + } -impl CredentialState { - pub fn name(&self) -> &str { - &self.name + /// Return a credential given its name + pub async fn get_credential_by_name(&self, name: &str) -> Result { + match self + .credentials_repository() + .await? + .get_credential(name) + .await? + { + Some(credential) => Ok(credential), + None => Err(CliStateError::ResourceNotFound { + name: name.to_string(), + resource: "credential".into(), + }), + } + } + + pub async fn get_credentials(&self) -> Result> { + Ok(self + .credentials_repository() + .await? + .get_credentials() + .await?) } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CredentialConfig { - pub issuer_identifier: Identifier, - // FIXME: Appear as array of number in JSON - pub encoded_issuer_change_history: Vec, - // FIXME: Appear as array of number in JSON - pub encoded_credential: Vec, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NamedCredential { + name: String, + issuer_identifier: Identifier, + issuer_change_history: ChangeHistory, + credential: CredentialAndPurposeKey, } -impl CredentialConfig { - pub fn new( +impl NamedCredential { + pub fn new(name: &str, issuer: &Identity, credential: CredentialAndPurposeKey) -> Self { + Self::make( + name, + issuer.identifier().clone(), + issuer.change_history().clone(), + credential, + ) + } + + pub fn make( + name: &str, issuer_identifier: Identifier, - encoded_issuer_change_history: Vec, - encoded_credential: Vec, - ) -> Result { - Ok(Self { + issuer_change_history: ChangeHistory, + credential: CredentialAndPurposeKey, + ) -> Self { + Self { + name: name.to_string(), issuer_identifier, - encoded_issuer_change_history, - encoded_credential, - }) + issuer_change_history, + credential, + } + } +} + +impl NamedCredential { + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn issuer_identifier(&self) -> Identifier { + self.issuer_identifier.clone() + } + + pub async fn issuer_identity(&self) -> Result { + Ok(Identity::create_from_change_history(&self.issuer_change_history).await?) } - pub fn credential(&self) -> Result { - minicbor::decode(&self.encoded_credential).map_err(|e| { - error!(%e, "Unable to decode credential"); - CliStateError::InvalidOperation("Unable to decode credential".to_string()) - }) + pub fn issuer_change_history(&self) -> ChangeHistory { + self.issuer_change_history.clone() + } + + pub fn credential_and_purpose_key(&self) -> CredentialAndPurposeKey { + self.credential.clone() } } -mod traits { +#[cfg(test)] +mod test { + use std::sync::Arc; + use std::time::Duration; + + use ockam::identity::models::CredentialSchemaIdentifier; + use ockam::identity::utils::AttributesBuilder; + use ockam::identity::{identities, Identities}; + use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for CredentialsState { - type Item = CredentialState; - const DEFAULT_FILENAME: &'static str = "credential"; - const DIR_NAME: &'static str = "credentials"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } - fn dir(&self) -> &PathBuf { - &self.dir - } - } + #[tokio::test] + async fn test_cli_spaces() -> Result<()> { + let cli = CliState::test().await?; + let identities = identities().await?; + let issuer_identifier = identities.identities_creation().create_identity().await?; + let issuer = identities.get_identity(&issuer_identifier).await?; + let credential = create_credential(identities, &issuer_identifier).await?; - #[async_trait] - impl StateItemTrait for CredentialState { - type Config = CredentialConfig; + // a credential can be stored and retrieved by name + cli.store_credential("name1", &issuer, credential.clone()) + .await?; + let result = cli.get_credential_by_name("name1").await?; + assert_eq!(result.name(), "name1".to_string()); + assert_eq!(result.issuer_identifier(), issuer_identifier); + assert_eq!(result.issuer_change_history(), *issuer.change_history()); + assert_eq!(result.credential_and_purpose_key(), credential); - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - Ok(Self { name, path, config }) - } + Ok(()) + } - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { name, path, config }) - } + /// HELPERS + async fn create_credential( + identities: Arc, + issuer: &Identifier, + ) -> Result { + let subject = identities.identities_creation().create_identity().await?; - fn path(&self) -> &PathBuf { - &self.path - } + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); - fn config(&self) -> &Self::Config { - &self.config - } + Ok(identities + .credentials() + .credentials_creation() + .issue_credential(issuer, &subject, attributes, Duration::from_secs(1)) + .await?) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs new file mode 100644 index 00000000000..498dcb64e0a --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs @@ -0,0 +1,153 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use ockam::identity::Identifier; +use ockam::identity::OneTimeCode; + +use crate::cli_state::Result; +use crate::cli_state::{CliState, CliStateError}; +use crate::cloud::project::Project; +use crate::error::ApiError; + +/// The following CliState methods help keeping track of +/// +impl CliState { + pub async fn is_identity_enrolled(&self, name: &Option) -> Result { + let repository = self.enrollment_repository().await?; + + match name { + Some(name) => Ok(repository.is_identity_enrolled(name).await?), + None => Ok(repository.is_default_identity_enrolled().await?), + } + } + + pub async fn is_default_identity_enrolled(&self) -> Result { + Ok(self + .enrollment_repository() + .await? + .is_default_identity_enrolled() + .await?) + } + + pub async fn set_identifier_as_enrolled(&self, identifier: &Identifier) -> Result<()> { + Ok(self + .enrollment_repository() + .await? + .set_as_enrolled(identifier) + .await?) + } + + /// Return information of enrolled entities. Either: + /// + /// - all the currently enrolled entities + /// - all the known identities and their corresponding enrollment state + pub async fn get_identity_enrollments( + &self, + enrollment_status: EnrollmentStatus, + ) -> Result> { + let repository = self.enrollment_repository().await?; + match enrollment_status { + EnrollmentStatus::Enrolled => Ok(repository.get_enrolled_identities().await?), + EnrollmentStatus::Any => Ok(repository.get_all_identities_enrollments().await?), + } + } + + /// Return true if the user is enrolled. + /// At the moment this check only verifies that there is a default project. + /// This project should be the project that is created at the end of the enrollment procedure + pub async fn is_enrolled(&self) -> miette::Result { + if !self.is_default_identity_enrolled().await? { + return Ok(false); + } + + let default_space_exists = self.get_default_space().await.is_ok(); + if !default_space_exists { + let message = + "There should be a default space set for the current user. Please re-enroll"; + error!("{}", message); + return Err(CliStateError::from(message).into()); + } + + let default_project_exists = self.get_default_project().await.is_ok(); + if !default_project_exists { + let message = + "There should be a default project set for the current user. Please re-enroll"; + error!("{}", message); + return Err(CliStateError::from(message).into()); + } + + Ok(true) + } +} + +pub enum EnrollmentStatus { + Enrolled, + Any, +} + +pub struct IdentityEnrollment { + identifier: Identifier, + name: Option, + is_default: bool, + enrolled_at: Option, +} + +impl IdentityEnrollment { + pub fn new( + identifier: Identifier, + name: Option, + is_default: bool, + enrolled_at: Option, + ) -> Self { + Self { + identifier, + name, + is_default, + enrolled_at, + } + } + pub fn identifier(&self) -> Identifier { + self.identifier.clone() + } + + #[allow(dead_code)] + pub fn name(&self) -> Option { + self.name.clone() + } + + #[allow(dead_code)] + pub fn is_enrolled(&self) -> bool { + self.enrolled_at.is_some() + } + + #[allow(dead_code)] + pub fn is_default(&self) -> bool { + self.is_default + } + + #[allow(dead_code)] + pub fn enrolled_at(&self) -> Option { + self.enrolled_at + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EnrollmentTicket { + pub one_time_code: OneTimeCode, + pub project: Option, +} + +impl EnrollmentTicket { + pub fn new(one_time_code: OneTimeCode, project: Option) -> Self { + Self { + one_time_code, + project, + } + } + + pub fn hex_encoded(&self) -> Result { + let serialized = serde_json::to_vec(&self) + .map_err(|_err| ApiError::core("Failed to authenticate with Okta"))?; + Ok(hex::encode(serialized)) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/error.rs b/implementations/rust/ockam/ockam_api/src/cli_state/error.rs new file mode 100644 index 00000000000..1899220670c --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/error.rs @@ -0,0 +1,73 @@ +use miette::Diagnostic; +use ockam_core::Error; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error, Diagnostic)] +pub enum CliStateError { + #[error(transparent)] + #[diagnostic(code("OCK500"))] + Io(#[from] std::io::Error), + + #[error(transparent)] + #[diagnostic(code("OCK500"))] + Serde(#[from] serde_json::Error), + + #[error(transparent)] + #[diagnostic(code("OCK500"))] + Ockam(#[from] ockam_core::Error), + + #[error("A {resource} named {name} already exists")] + #[diagnostic( + code("OCK409"), + help("Please try using a different name or delete the existing {resource}") + )] + AlreadyExists { resource: String, name: String }, + + #[error("Unable to find {resource} named {name}")] + #[diagnostic(code("OCK404"))] + ResourceNotFound { resource: String, name: String }, + + #[error("The path {0} is invalid")] + #[diagnostic(code("OCK500"))] + InvalidPath(String), + + #[error("The path is empty")] + #[diagnostic(code("OCK500"))] + EmptyPath, + + #[error("{0}")] + #[diagnostic(code("OCK500"))] + InvalidData(String), + + #[error("{0}")] + #[diagnostic(code("OCK500"))] + InvalidOperation(String), + + #[error("Invalid configuration version '{0}'")] + #[diagnostic( + code("OCK500"), + help("Please try running 'ockam reset' to reset your local configuration") + )] + InvalidVersion(String), +} + +impl From<&str> for CliStateError { + fn from(e: &str) -> Self { + CliStateError::InvalidOperation(e.to_string()) + } +} + +impl From for ockam_core::Error { + fn from(e: CliStateError) -> Self { + match e { + CliStateError::Ockam(e) => e, + _ => Error::new( + ockam_core::errcode::Origin::Application, + ockam_core::errcode::Kind::Internal, + e, + ), + } + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs b/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs index ca4c37a9cf8..b9085748a74 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs @@ -1,350 +1,551 @@ -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::SystemTime; - -use serde::{Deserialize, Serialize}; -use time::format_description::well_known::Iso8601; -use time::OffsetDateTime; - -use ockam::identity::storage::LmdbStorage; -use ockam::identity::{Identifier, IdentitiesRepository, IdentitiesStorage}; - -use crate::cli_state::traits::{StateDirTrait, StateItemTrait}; -use crate::cli_state::{CliStateError, DATA_DIR_NAME}; +use ockam::identity::models::ChangeHistory; +use ockam::identity::{Identifier, Identity}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_vault::{HandleToSecret, SigningSecretKeyHandle}; + +use crate::cli_state::{random_name, CliState, Result}; + +/// The methods below allow the creation named identities. +/// A NamedIdentity is an identity that is associated to a name in order to be more easily +/// retrieved when necessary. +/// +/// A NamedIdentity can also be set as the default identity so that it is implicitly picked up +/// when running some commands: +/// +/// - the first created identity is always the default one. +/// - when we need to use the default identity, it is created in case it did not exist before +/// - the name of the default identity, if it has been created implicitly is always "default" +/// +/// In order to create an identity we need to have a Vault: +/// +/// - if a vault has already been created, the vault name can be provided +/// - otherwise we used the default vault, which is created if it does not exist the first time we need it +/// +impl CliState { + /// Create an identity associated with a name and a specific vault name + /// If there is already an identity with that name, return its identifier + pub async fn create_identity_with_name_and_vault( + &self, + name: &str, + vault_name: &str, + ) -> Result { + if let Ok(identity) = self.get_named_identity(name).await { + return Ok(identity); + }; + + let vault = self.get_named_vault(vault_name).await?; + let identities = self.make_identities(vault.vault().await?).await?; + let identity = identities.identities_creation().create_identity().await?; + + self.store_named_identity(&identity, name, &vault.name()) + .await + } -use super::Result; + /// Create an identity associated with a name, using the default vault + /// If there is already an identity with that name, return its identifier + pub async fn create_identity_with_name(&self, name: &str) -> Result { + let vault = self.get_default_named_vault().await?; + self.create_identity_with_name_and_vault(name, &vault.name()) + .await + } + /// Create an identity associated with an optional name and an optional vault name + /// If the vault name is not specified then the default vault is used + pub async fn create_identity_with_optional_name_and_optional_vault( + &self, + name: &Option, + vault_name: &Option, + ) -> Result { + let name = name.clone().unwrap_or_else(random_name); + let vault = self.get_named_vault_or_default(vault_name).await?.name(); + self.create_identity_with_name_and_vault(&name, &vault) + .await + } -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct IdentitiesState { - dir: PathBuf, + /// Create an identity with specific key id. + /// This method is used when the vault is a KMS vault and we just need to store a key id + /// for the identity key existing in the KMS + pub async fn create_identity_with_key_id( + &self, + name: &str, + vault_name: &str, + key_id: &str, + ) -> Result { + let vault = self.get_named_vault(vault_name).await?; + + // Check that the vault is an KMS vault + if !vault.is_kms() { + return Err(Error::new( + Origin::Api, + Kind::Misuse, + format!("Vault {vault_name} is not a KMS vault"), + ) + .into()); + }; + + let handle = SigningSecretKeyHandle::ECDSASHA256CurveP256(HandleToSecret::new( + key_id.as_bytes().to_vec(), + )); + + // create the identity + let identities = self.make_identities(vault.vault().await?).await?; + let identifier = identities + .identities_creation() + .identity_builder() + .with_existing_key(handle) + .build() + .await? + .clone(); + + self.store_named_identity(&identifier, name, vault_name) + .await + } } -impl IdentitiesState { - pub fn get_or_default(&self, name: Option<&str>) -> Result { - if let Some(identity_name) = name { - self.get(identity_name) - } else { - self.default() +/// The methods below allow to query identities: +/// +/// - all of them +/// - one identity by name +/// - the identifier of a named identity +/// - etc... +/// +/// Note that these methods return an Error when an identity is not found. +/// We assume, when using them that there is already an identity created with a given name. +/// +impl CliState { + /// Return all named identities + pub async fn get_named_identities(&self) -> Result> { + Ok(self + .identities_repository() + .await? + .get_named_identities() + .await?) + } + + /// Return a named identity given its name + pub async fn get_named_identity(&self, name: &str) -> Result { + let repository = self.identities_repository().await?; + match repository.get_named_identity(name).await? { + Some(identity) => Ok(identity), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("no identity found with name {}", name), + ) + .into()), } } - pub fn get_by_identifier(&self, identifier: &Identifier) -> Result { - self.list()? - .into_iter() - .find(|ident_state| &ident_state.config.identifier() == identifier) - .ok_or(CliStateError::ResourceNotFound { - resource: Self::default_filename().to_string(), - name: identifier.to_string(), - }) + /// Return a named identity given its name or the default named identity + pub async fn get_named_identity_or_default( + &self, + name: &Option, + ) -> Result { + match name { + Some(name) => self.get_named_identity(name).await, + None => self.get_default_named_identity().await, + } } - pub async fn identities_repository(&self) -> Result> { - let lmdb_path = self.identities_repository_path()?; - Ok(Arc::new(IdentitiesStorage::new(Arc::new( - LmdbStorage::new(lmdb_path).await?, - )))) + /// Return the identifier of a named identity + pub async fn get_identifier_by_name(&self, name: &str) -> Result { + Ok(self.get_named_identity(name).await?.identifier()) } - pub fn identities_repository_path(&self) -> Result { - let lmdb_path = self - .dir - .join(DATA_DIR_NAME) - .join("authenticated_storage.lmdb"); - Ok(lmdb_path) + /// Return the identifier for identity given an optional name. + /// If that name is None, then we return the identifier of the default identity + pub async fn get_identifier_by_optional_name( + &self, + name: &Option, + ) -> Result { + let repository = self.identities_repository().await?; + let result = match name { + Some(name) => repository.get_identifier(name).await?, + None => repository + .get_default_named_identity() + .await? + .map(|i| i.identifier()), + }; + + result.ok_or_else(|| Self::missing_identifier(name).into()) } -} -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct IdentityState { - name: String, - path: PathBuf, - /// The path to the directory containing the authenticated storage files, shared amongst all identities - data_path: PathBuf, - config: IdentityConfig, -} + /// Return a full identity from its name + /// Use the default identity if no name is given + pub async fn get_identity_by_optional_name(&self, name: &Option) -> Result { + let named_identity = match name { + Some(name) => { + self.identities_repository() + .await? + .get_named_identity(name) + .await? + } -impl IdentityState { - pub fn identifier(&self) -> Identifier { - self.config.identifier() + None => { + self.identities_repository() + .await? + .get_default_named_identity() + .await? + } + }; + match named_identity { + Some(identity) => { + let change_history = self.get_change_history(&identity.identifier()).await?; + Ok(Identity::import_from_change_history( + Some(&identity.identifier()), + change_history, + self.get_default_named_vault() + .await? + .vault() + .await? + .verifying_vault, + ) + .await?) + } + None => Err(Self::missing_identifier(name).into()), + } } - pub fn set_enrollment_status(&mut self) -> Result<()> { - self.config.enrollment_status = Some(EnrollmentStatus::enrolled()); - self.persist() + /// Return the identity with the given identifier + pub async fn get_identity(&self, identifier: &Identifier) -> Result { + match self + .change_history_repository() + .await? + .get_change_history(identifier) + .await? + { + Some(change_history) => { + Ok(Identity::create_from_change_history(&change_history).await?) + } + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("no identity found for identifier {identifier}"), + ) + .into()), + } } - fn build_data_path(path: &Path) -> PathBuf { - path.parent() - .expect("Should have parent") - .join(DATA_DIR_NAME) + /// Return the name of the default identity. + /// This function creates the default identity if it does not exist! + pub async fn get_default_identity_name(&self) -> Result { + Ok(self.get_default_named_identity().await?.name()) } - pub fn name(&self) -> &str { - &self.name + /// Return the default named identity + /// This function creates the default identity if it does not exist! + pub async fn get_default_named_identity(&self) -> Result { + match self + .identities_repository() + .await? + .get_default_named_identity() + .await? + { + Some(named_identity) => Ok(named_identity), + None => self.create_identity_with_name(&random_name()).await, + } } - pub fn is_enrolled(&self) -> bool { - self.config - .enrollment_status - .as_ref() - .map(|s| s.is_enrolled) - .unwrap_or(false) + /// Return: + /// - the given name if defined + /// - or the name of the default identity (which is created if it does not already exist!) + pub async fn get_identity_name_or_default(&self, name: &Option) -> Result { + match name { + Some(name) => Ok(name.clone()), + None => self.get_default_identity_name().await, + } } -} -impl Display for IdentityState { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!( - f, - "Name: {}", - self.path.as_path().file_stem().unwrap().to_str().unwrap() - )?; - writeln!(f, "State Path: {}", self.path.clone().to_str().unwrap())?; - writeln!(f, "Config Identifier: {}", self.config.identifier())?; - match &self.config.enrollment_status { - Some(enrollment) => { - writeln!(f, "Enrollment Status:")?; - for line in enrollment.to_string().lines() { - writeln!(f, "{:2}{}", "", line)?; - } - } - None => (), + /// Return the named identity with the given identifier + pub async fn get_named_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result { + match self + .identities_repository() + .await? + .get_named_identity_by_identifier(identifier) + .await? + { + Some(named_identity) => Ok(named_identity), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("no named identity found for identifier {identifier}"), + ) + .into()), } - Ok(()) } -} -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct IdentityConfig { - pub identifier: Identifier, - pub enrollment_status: Option, + /// Return true if there is an identity with that name and it is the default one + pub async fn is_default_identity_by_name(&self, name: &str) -> Result { + Ok(self + .identities_repository() + .await? + .get_named_identity(name) + .await? + .map(|n| n.is_default()) + .unwrap_or(false)) + } } -impl PartialEq for IdentityConfig { - fn eq(&self, other: &Self) -> bool { - self.identifier == other.identifier +/// The following methods allow to update existing named identities +impl CliState { + /// Set a named identity as the default + /// Return an error if that identity does not exist + pub async fn set_as_default_identity(&self, name: &str) -> Result<()> { + Ok(self + .identities_repository() + .await? + .set_as_default(name) + .await?) + } + + /// Delete an identity by name: + /// + /// - check that the identity is not used by a node first + /// - then remove the the name association to the identity + /// - and remove the identity change history + /// + pub async fn delete_identity_by_name(&self, name: &str) -> Result<()> { + let nodes = self.get_nodes_by_identity_name(name).await?; + if nodes.is_empty() { + if let Some(identifier) = self + .identities_repository() + .await? + .delete_identity(name) + .await? + { + self.change_history_repository() + .await? + .delete_change_history(&identifier) + .await?; + }; + Ok(()) + } else { + let node_names: Vec = nodes.iter().map(|n| n.name()).collect(); + Err(Error::new( + Origin::Api, + Kind::Invalid, + format!( + "The identity named {name} cannot be deleted because it is used by the node(s): {}", + node_names.join(", ") + ), + ) + .into()) + } } } -impl Eq for IdentityConfig {} +/// Support methods +impl CliState { + /// Once a identity has been created, store it. + /// If there is no previous default identity we set it as the default identity + async fn store_named_identity( + &self, + identifier: &Identifier, + name: &str, + vault_name: &str, + ) -> Result { + let repository = self.identities_repository().await?; + + // If there is no previously created identity we set this identity as the default one + let is_default_identity = repository.get_default_named_identity().await?.is_none(); + let mut named_identity = repository + .store_named_identity(identifier, name, vault_name) + .await?; + if is_default_identity { + repository + .set_as_default_by_identifier(&named_identity.identifier()) + .await?; + named_identity = named_identity.set_as_default(); + } + Ok(named_identity) + } -impl IdentityConfig { - pub async fn new(identifier: &Identifier) -> Self { - Self { - identifier: identifier.clone(), - enrollment_status: None, + /// Return the change history of a persisted identity + async fn get_change_history(&self, identifier: &Identifier) -> Result { + match self + .change_history_repository() + .await? + .get_change_history(identifier) + .await? + { + Some(change_history) => Ok(change_history), + None => Err(Error::new( + Origin::Core, + Kind::NotFound, + format!("identity not found for identifier {}", identifier), + ) + .into()), } } - pub fn identifier(&self) -> Identifier { - self.identifier.clone() + fn missing_identifier(name: &Option) -> Error { + let message = name + .clone() + .map_or("no default identifier found".to_string(), |n| { + format!("no identifier found with name {}", n) + }); + Error::new(Origin::Api, Kind::NotFound, message) } } -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EnrollmentStatus { - pub is_enrolled: bool, - pub created_at: SystemTime, +/// A named identity associates a name with a persisted identity. +/// This is a convenience for users since they can refer to an identity by the name "alice" +/// instead of the identifier "I1234561234561234561234561234561234561234" +/// +/// Additionally one identity can be marked as being the default identity and taken to +/// establish a secure channel or create credentials without having to specify it. +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct NamedIdentity { + identifier: Identifier, + name: String, + vault_name: String, + is_default: bool, } -impl EnrollmentStatus { - pub fn enrolled() -> EnrollmentStatus { - EnrollmentStatus { - is_enrolled: true, - created_at: SystemTime::now(), +impl NamedIdentity { + /// Create a new named identity + pub fn new(identifier: Identifier, name: String, vault_name: String, is_default: bool) -> Self { + Self { + identifier, + name, + vault_name, + is_default, } } -} -impl Display for EnrollmentStatus { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.is_enrolled { - writeln!(f, "Enrolled: yes")?; - } else { - writeln!(f, "Enrolled: no")?; - } + /// Return the identity identifier + pub fn identifier(&self) -> Identifier { + self.identifier.clone() + } - match OffsetDateTime::from(self.created_at).format(&Iso8601::DEFAULT) { - Ok(time_str) => writeln!(f, "Timestamp: {}", time_str)?, - Err(err) => writeln!( - f, - "Error formatting OffsetDateTime as Iso8601 String: {}", - err - )?, - } + /// Return the identity name + pub fn name(&self) -> String { + self.name.clone() + } - Ok(()) + /// Return the vault name + pub fn vault_name(&self) -> String { + self.vault_name.clone() } -} -// TODO: No longer supported: consider deleting -#[derive(Deserialize, Debug, Clone)] -struct IdentityConfigV1 { - // Easiest way to fail deserialization - _non_existent_field: bool, -} + /// Return true if this identity is the default one + pub fn is_default(&self) -> bool { + self.is_default + } -// TODO: No longer supported: consider deleting -#[derive(Deserialize, Debug, Clone)] -struct IdentityConfigV2 { - // Easiest way to fail deserialization - _non_existent_field: bool, + /// Return a NamedIdentity where is_default is set to true + pub fn set_as_default(&self) -> NamedIdentity { + let mut result = self.clone(); + result.is_default = true; + result + } } -// TODO: No longer supported: consider deleting -#[derive(Deserialize, Debug, Clone)] -struct IdentityConfigV3 { - // Easiest way to fail deserialization - _non_existent_field: bool, -} +#[cfg(test)] +mod tests { + use super::*; -#[derive(Deserialize, Debug, Clone)] -#[serde(untagged)] -enum IdentityConfigs { - V1(IdentityConfigV1), - V2(IdentityConfigV2), - V3(IdentityConfigV3), - V4(IdentityConfig), -} + #[tokio::test] + async fn test_create_identity_with_a_vault() -> Result<()> { + let cli = CliState::test().await?; + + // create a vault first + let vault_name = "vault-name"; + let _ = cli.create_named_vault(vault_name).await?; + + // then create an identity + let identity_name = "identity-name"; + let identity = cli + .create_identity_with_name_and_vault(identity_name, vault_name) + .await?; + let expected = cli.get_named_identity(identity_name).await?; + assert_eq!(identity, expected); + + // don't recreate the identity if it already exists with that name + let _ = cli + .create_identity_with_name_and_vault(identity_name, vault_name) + .await?; + let identities = cli.get_named_identities().await?; + assert_eq!(identities.len(), 1); -mod traits { - use ockam_core::async_trait; + Ok(()) + } - use crate::cli_state::traits::*; - use crate::cli_state::{file_stem, CliStateError}; + #[tokio::test] + async fn test_create_identity_with_the_default_vault() -> Result<()> { + let cli = CliState::test().await?; - use super::*; + // create an identity using the default vault + let identity_name = "identity-name"; + let identity = cli.create_identity_with_name(identity_name).await?; + let expected = cli.get_named_identity(identity_name).await?; + assert_eq!(identity, expected); - #[async_trait] - impl StateDirTrait for IdentitiesState { - type Item = IdentityState; - const DEFAULT_FILENAME: &'static str = "identity"; - const DIR_NAME: &'static str = "identities"; - const HAS_DATA_DIR: bool = true; + // check that the identity uses the default vault + let default_vault = cli.get_default_named_vault().await?; + assert_eq!(identity.vault_name, default_vault.name()); - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } + Ok(()) + } - fn dir(&self) -> &PathBuf { - &self.dir - } + #[tokio::test] + async fn test_create_default_identity() -> Result<()> { + let cli = CliState::test().await?; - fn delete(&self, name: impl AsRef) -> Result<()> { - // Retrieve identity. If doesn't exist do nothing. - let identity = match self.get(&name) { - Ok(i) => i, - Err(CliStateError::ResourceNotFound { .. }) => return Ok(()), - Err(e) => return Err(e), - }; + // create a default identity using the default vault + let identity = cli + .create_identity_with_optional_name_and_optional_vault(&None, &None) + .await?; + let expected = cli.get_default_named_identity().await?; + assert_eq!(identity, expected); - // If it's the default, remove link - if let Ok(default) = self.default() { - if default.path == identity.path { - let _ = std::fs::remove_file(self.default_path()?); - } - } - // Remove identity file - identity.delete()?; - Ok(()) - } + // check that the identity uses the default vault + let default_vault = cli.get_default_named_vault().await?; + assert_eq!(identity.vault_name, default_vault.name()); - async fn migrate(&self, path: &Path) -> Result<()> { - let contents = std::fs::read_to_string(path)?; - - // read the configuration and migrate to the most recent format if an old format is found - // the most recent configuration only contains an identity identifier, so if we find an - // old format we store the full identity in the shared identities repository before - // writing the most recent configuration format - match serde_json::from_str(&contents)? { - IdentityConfigs::V1(_) | IdentityConfigs::V2(_) | IdentityConfigs::V3(_) => { - return Err(CliStateError::InvalidVersion( - "Migration not supported for old Identities".to_string(), - )) - } - IdentityConfigs::V4(_) => {} - } - Ok(()) - } + Ok(()) } - #[async_trait] - impl StateItemTrait for IdentityState { - type Config = IdentityConfig; - - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - let data_path = IdentityState::build_data_path(&path); - Ok(Self { - name, - path, - data_path, - config, - }) - } + #[tokio::test] + async fn test_get_default_identity() -> Result<()> { + let cli = CliState::test().await?; - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - let data_path = IdentityState::build_data_path(&path); - Ok(Self { - name, - path, - data_path, - config, - }) - } + // when we retrieve the default identity, we create it if it doesn't exist + let identity = cli.get_default_named_identity().await?; - fn path(&self) -> &PathBuf { - &self.path - } + // when the identity is created there is a change history + a named identity + let result = cli.get_change_history(&identity.identifier()).await; + assert!(result.is_ok()); - fn config(&self) -> &Self::Config { - &self.config - } + let result = cli.get_named_identity(&identity.name()).await; + assert!(result.is_ok()); + + Ok(()) } -} -#[cfg(test)] -mod tests { - use super::*; + #[tokio::test] + async fn test_delete_identity() -> Result<()> { + let cli = CliState::test().await?; + let identity = cli.create_identity_with_name("name").await?; - #[test] - fn test_serialize() { - let identity_config = create_identity_config(); - let expected = create_identity_config_json(); - assert_eq!(serde_json::to_string(&identity_config).unwrap(), expected) - } + // when the identity is created there is a change history + a named identity + let result = cli.get_change_history(&identity.identifier()).await; + assert!(result.is_ok()); - #[test] - fn test_deserialize() { - let json = create_identity_config_json(); - let actual: IdentityConfig = serde_json::from_str(json.as_str()).unwrap(); - let expected = create_identity_config(); - assert_eq!(actual, expected) - } + let result = cli.get_named_identity(&identity.name()).await; + assert!(result.is_ok()); - fn create_identity_config() -> IdentityConfig { - let identifier = Identifier::try_from( - "Ifa804b7fca12a19eed206ae180b5b576860ae651a1b2c3d4e5f6a6b5c4d3e2f1", - ) - .unwrap(); - IdentityConfig { - identifier, - enrollment_status: Some(EnrollmentStatus { - is_enrolled: true, - created_at: SystemTime::from(OffsetDateTime::from_unix_timestamp(0).unwrap()), - }), - } - } + // now if we delete the identity there is no more persisted data + cli.delete_identity_by_name(&identity.name()).await?; + let result = cli.get_change_history(&identity.identifier()).await; + assert!(result.is_err()); - fn create_identity_config_json() -> String { - r#"{"identifier":"Ifa804b7fca12a19eed206ae180b5b576860ae651a1b2c3d4e5f6a6b5c4d3e2f1","enrollment_status":{"is_enrolled":true,"created_at":{"secs_since_epoch":0,"nanos_since_epoch":0}}}"#.into() + let result = cli.get_named_identity(&identity.name()).await; + assert!(result.is_err()); + + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs b/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs index b8ba6b1d359..8e9e5e9c11a 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs @@ -1,784 +1,33 @@ +pub use cli_state::*; +pub use credentials::*; +pub use enrollments::*; +pub use error::*; +pub use identities::*; +pub use nodes::*; +pub use policies::*; +pub use projects::*; +pub use secure_channels::*; +pub use spaces::*; +pub use storage::*; +pub use test_support::*; +pub use trust_contexts::*; +pub use users::*; +pub use vaults::*; + +#[allow(clippy::module_inception)] +pub mod cli_state; pub mod credentials; +pub mod enrollments; +pub mod error; pub mod identities; pub mod nodes; +pub mod policies; pub mod projects; +pub mod repositories; +pub mod secure_channels; pub mod spaces; -pub mod traits; +pub mod storage; +pub mod test_support; pub mod trust_contexts; -pub mod user_info; +pub mod users; pub mod vaults; - -pub use crate::cli_state::credentials::*; -pub use crate::cli_state::identities::*; -pub use crate::cli_state::nodes::*; -pub use crate::cli_state::projects::*; -pub use crate::cli_state::spaces::*; -pub use crate::cli_state::traits::*; -pub use crate::cli_state::trust_contexts::*; -use crate::cli_state::user_info::UsersInfoState; -pub use crate::cli_state::vaults::*; -use crate::config::cli::LegacyCliConfig; -use miette::Diagnostic; -use ockam::identity::Identifier; -use ockam::identity::Identities; -use ockam::identity::Vault; -use ockam_core::compat::sync::Arc; -use ockam_core::env::get_env_with_default; -use ockam_node::Executor; -use rand::random; -use std::path::{Path, PathBuf}; -use thiserror::Error; - -type Result = std::result::Result; - -#[derive(Debug, Error, Diagnostic)] -pub enum CliStateError { - #[error(transparent)] - #[diagnostic(code("OCK500"))] - Io(#[from] std::io::Error), - - #[error(transparent)] - #[diagnostic(code("OCK500"))] - Serde(#[from] serde_json::Error), - - #[error(transparent)] - #[diagnostic(code("OCK500"))] - Ockam(#[from] ockam_core::Error), - - #[error("A {resource} named {name} already exists")] - #[diagnostic( - code("OCK409"), - help("Please try using a different name or delete the existing {resource}") - )] - AlreadyExists { resource: String, name: String }, - - #[error("Unable to find {resource} named {name}")] - #[diagnostic(code("OCK404"))] - ResourceNotFound { resource: String, name: String }, - - #[error("The path {0} is invalid")] - #[diagnostic(code("OCK500"))] - InvalidPath(String), - - #[error("The path is empty")] - #[diagnostic(code("OCK500"))] - EmptyPath, - - #[error("{0}")] - #[diagnostic(code("OCK500"))] - InvalidData(String), - - #[error("{0}")] - #[diagnostic(code("OCK500"))] - InvalidOperation(String), - - #[error("Invalid configuration version '{0}'")] - #[diagnostic( - code("OCK500"), - help("Please try running 'ockam reset' to reset your local configuration") - )] - InvalidVersion(String), -} - -impl From<&str> for CliStateError { - fn from(e: &str) -> Self { - CliStateError::InvalidOperation(e.to_string()) - } -} - -impl From for ockam_core::Error { - fn from(e: CliStateError) -> Self { - match e { - CliStateError::Ockam(e) => e, - _ => ockam_core::Error::new( - ockam_core::errcode::Origin::Application, - ockam_core::errcode::Kind::Internal, - e, - ), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct CliState { - pub vaults: VaultsState, - pub identities: IdentitiesState, - pub nodes: NodesState, - pub spaces: SpacesState, - pub projects: ProjectsState, - pub credentials: CredentialsState, - pub trust_contexts: TrustContextsState, - pub users_info: UsersInfoState, - pub dir: PathBuf, -} - -impl CliState { - /// Return an initialized CliState - /// There should only be one call to this function since it also performs a migration - /// of configuration files if necessary - pub fn initialize() -> Result { - let dir = Self::default_dir()?; - std::fs::create_dir_all(dir.join("defaults"))?; - Executor::execute_future(Self::initialize_cli_state())? - } - - /// Create a new CliState by initializing all of its components - /// The calls to 'init(dir)' are loading each piece of configuration and possibly doing some - /// configuration migration if necessary - async fn initialize_cli_state() -> Result { - let default = Self::default_dir()?; - let dir = default.as_path(); - let state = Self { - vaults: VaultsState::init(dir).await?, - identities: IdentitiesState::init(dir).await?, - nodes: NodesState::init(dir).await?, - spaces: SpacesState::init(dir).await?, - projects: ProjectsState::init(dir).await?, - credentials: CredentialsState::init(dir).await?, - trust_contexts: TrustContextsState::init(dir).await?, - users_info: UsersInfoState::init(dir).await?, - dir: dir.to_path_buf(), - }; - state.migrate()?; - Ok(state) - } - - /// Reset all directories and return a new CliState - pub async fn reset(&self) -> Result { - Self::delete_at(&self.dir)?; - Self::initialize_cli_state().await - } - - pub fn backup_and_reset() -> Result { - let dir = Self::default_dir()?; - - // Reset backup directory - let backup_dir = Self::backup_default_dir()?; - if backup_dir.exists() { - let _ = std::fs::remove_dir_all(&backup_dir); - } - std::fs::create_dir_all(&backup_dir)?; - - // Move state to backup directory - for entry in std::fs::read_dir(&dir)? { - let entry = entry?; - let from = entry.path(); - let to = backup_dir.join(entry.file_name()); - std::fs::rename(from, to)?; - } - - // Reset state - Self::delete_at(&dir)?; - Self::initialize() - } - - fn migrate(&self) -> Result<()> { - // If there is a `config.json` file, migrate its contents to the spaces and project states. - let legacy_config_path = self.dir.join("config.json"); - if legacy_config_path.exists() { - let contents = std::fs::read_to_string(&legacy_config_path)?; - let legacy_config: LegacyCliConfig = serde_json::from_str(&contents)?; - let spaces = self.spaces.list()?; - for (name, lookup) in legacy_config.lookup.spaces() { - if !spaces.iter().any(|s| s.name() == name) { - let config = SpaceConfig::from_lookup(&name, lookup); - self.spaces.create(name, config)?; - } - } - let projects = self.projects.list()?; - for (name, lookup) in legacy_config.lookup.projects() { - if !projects.iter().any(|p| p.name() == name) { - self.projects.create(name, lookup.into())?; - } - } - std::fs::remove_file(legacy_config_path)?; - } - Ok(()) - } - - pub fn delete_at(root_path: &PathBuf) -> Result<()> { - // Delete nodes' state and processes, if possible - let nodes_state = NodesState::new(root_path); - let _ = nodes_state.list().map(|nodes| { - nodes.iter().for_each(|n| { - let _ = n.delete_sigkill(true); - }); - }); - - // Delete all other state directories - for dir in &[ - nodes_state.dir(), - IdentitiesState::new(root_path).dir(), - VaultsState::new(root_path).dir(), - SpacesState::new(root_path).dir(), - ProjectsState::new(root_path).dir(), - CredentialsState::new(root_path).dir(), - TrustContextsState::new(root_path).dir(), - UsersInfoState::new(root_path).dir(), - &root_path.join("defaults"), - ] { - let _ = std::fs::remove_dir_all(dir); - } - - // Delete config files located at the root of the state directory - let config_file = root_path.join("config.json"); - let _ = std::fs::remove_file(config_file); - - // If the state directory is now empty, delete it - let is_empty = std::fs::read_dir(root_path) - .map(|mut d| d.next().is_none()) - .unwrap_or(false); - if is_empty { - let _ = std::fs::remove_dir(root_path); - } - - Ok(()) - } - - pub fn delete() -> Result<()> { - Self::delete_at(&Self::default_dir()?) - } - - pub fn delete_identity(&self, identity_state: IdentityState) -> Result<()> { - // Abort if identity is being used by some running node. - for node in self.nodes.list()? { - if node.config().identity_config()?.identifier() == identity_state.identifier() { - return Err(CliStateError::InvalidOperation(format!( - "Can't delete identity '{}' as it's being used by node '{}'", - &identity_state.name(), - &node.name() - ))); - } - } - identity_state.delete() - } - - /// Returns the default directory for the CLI state. - pub fn default_dir() -> Result { - Ok(get_env_with_default::( - "OCKAM_HOME", - home::home_dir() - .ok_or(CliStateError::InvalidPath("$HOME".to_string()))? - .join(".ockam"), - )?) - } - - /// Returns the default backup directory for the CLI state. - pub fn backup_default_dir() -> Result { - let dir = Self::default_dir()?; - let dir_name = - dir.file_name() - .and_then(|n| n.to_str()) - .ok_or(CliStateError::InvalidOperation( - "The $OCKAM_HOME directory does not have a valid name".to_string(), - ))?; - let parent = dir.parent().ok_or(CliStateError::InvalidOperation( - "The $OCKAM_HOME directory does not a valid parent directory".to_string(), - ))?; - Ok(parent.join(format!("{dir_name}.bak"))) - } - - /// Returns the directory where the default objects are stored. - fn defaults_dir(dir: &Path) -> Result { - Ok(dir.join("defaults")) - } - - pub async fn create_vault_state(&self, vault_name: Option<&str>) -> Result { - // Try to get the vault with the given name - let vault_state = if let Some(v) = vault_name { - self.vaults.get(v)? - } - // Or get the default - else if let Ok(v) = self.vaults.default() { - v - } - // Or create a new one with a random name - else { - let n = random_name(); - let c = VaultConfig::default(); - self.vaults.create_async(&n, c).await? - }; - Ok(vault_state) - } - - pub async fn create_identity_state( - &self, - identifier: &Identifier, - identity_name: Option<&str>, - ) -> Result { - if let Ok(identity) = self.identities.get_or_default(identity_name) { - Ok(identity) - } else { - self.make_identity_state(identifier, identity_name).await - } - } - - async fn make_identity_state( - &self, - identifier: &Identifier, - name: Option<&str>, - ) -> Result { - let identity_config = IdentityConfig::new(identifier).await; - let identity_name = name.map(|x| x.to_string()).unwrap_or_else(random_name); - self.identities.create(identity_name, identity_config) - } - - pub async fn get_identities(&self, vault: Vault) -> Result> { - Ok(Identities::builder() - .with_vault(vault) - .with_identities_repository(self.identities.identities_repository().await?) - .build()) - } - - pub async fn default_identities(&self) -> Result> { - Ok(Identities::builder() - .with_vault(self.vaults.default()?.vault().await?) - .with_identities_repository(self.identities.identities_repository().await?) - .build()) - } - - /// Return true if the user is enrolled. - /// At the moment this check only verifies that there is a default project. - /// This project should be the project that is created at the end of the enrollment procedure - pub fn is_enrolled(&self) -> Result { - let identity_state = self.identities.default()?; - if !identity_state.is_enrolled() { - return Ok(false); - } - - let default_space_exists = self.spaces.default().is_ok(); - if !default_space_exists { - let message = - "There should be a default space set for the current user. Please re-enroll"; - error!("{}", message); - return Err(message.into()); - } - - let default_project_exists = self.projects.default().is_ok(); - if !default_project_exists { - let message = - "There should be a default project set for the current user. Please re-enroll"; - error!("{}", message); - return Err(message.into()); - } - - Ok(true) - } -} - -/// Test support -impl CliState { - #[cfg(test)] - /// Initialize CliState at the given directory - async fn initialize_at(dir: &Path) -> Result { - std::fs::create_dir_all(dir.join("defaults"))?; - let state = Self { - vaults: VaultsState::init(dir).await?, - identities: IdentitiesState::init(dir).await?, - nodes: NodesState::init(dir).await?, - spaces: SpacesState::init(dir).await?, - projects: ProjectsState::init(dir).await?, - credentials: CredentialsState::init(dir).await?, - trust_contexts: TrustContextsState::init(dir).await?, - users_info: UsersInfoState::init(dir).await?, - dir: dir.to_path_buf(), - }; - state.migrate()?; - Ok(state) - } - - /// Create a new CliState (but do not run migrations) - fn new(dir: &Path) -> Result { - std::fs::create_dir_all(dir.join("defaults"))?; - Ok(Self { - vaults: VaultsState::load(dir)?, - identities: IdentitiesState::load(dir)?, - nodes: NodesState::load(dir)?, - spaces: SpacesState::load(dir)?, - projects: ProjectsState::load(dir)?, - credentials: CredentialsState::load(dir)?, - trust_contexts: TrustContextsState::load(dir)?, - users_info: UsersInfoState::load(dir)?, - dir: dir.to_path_buf(), - }) - } - - /// Return a test CliState with a random root directory - pub fn test() -> Result { - Self::new(&Self::test_dir()?) - } - - /// Return a random root directory - pub fn test_dir() -> Result { - Ok(home::home_dir() - .ok_or(CliStateError::InvalidPath("$HOME".to_string()))? - .join(".ockam") - .join(".tests") - .join(random_name())) - } -} - -pub fn random_name() -> String { - petname::petname(2, "-").unwrap_or(hex::encode(random::<[u8; 4]>())) -} - -fn file_stem(path: &Path) -> Result { - let path_str = path.to_str().ok_or(CliStateError::EmptyPath)?; - path.file_stem() - .ok_or(CliStateError::InvalidPath(path_str.to_string()))? - .to_str() - .map(|name| name.to_string()) - .ok_or(CliStateError::InvalidPath(path_str.to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::cloud::enroll::auth0::UserInfo; - use crate::config::cli::TrustContextConfig; - use crate::config::lookup::{ConfigLookup, LookupValue, ProjectLookup, SpaceLookup}; - use ockam_core::compat::rand::random_string; - use ockam_multiaddr::MultiAddr; - use std::str::FromStr; - - #[tokio::test] - async fn test_create_default_identity_state() { - let state = CliState::test().unwrap(); - let identifier = "Ie92f183eb4c324804ef4d62962dea94cf095a265a1b2c3d4e5f6a6b5c4d3e2f1" - .try_into() - .unwrap(); - let identity1 = state - .create_identity_state(&identifier, None) - .await - .unwrap(); - let identity2 = state - .create_identity_state(&identifier, None) - .await - .unwrap(); - - let default_identity = state.identities.default().unwrap(); - assert_eq!(identity1, default_identity); - - // make sure that a default identity is not recreated twice - assert_eq!(identity1.name(), identity2.name()); - assert_eq!(identity1.path(), identity2.path()); - } - - #[tokio::test] - async fn test_create_named_identity_state() { - let state = CliState::test().unwrap(); - let alice = "Ie92f183eb4c324804ef4d62962dea94cf095a265a1b2c3d4e5f6a6b5c4d3e2f1" - .try_into() - .unwrap(); - let identity1 = state - .create_identity_state(&alice, Some("alice")) - .await - .unwrap(); - let identity2 = state - .create_identity_state(&alice, Some("alice")) - .await - .unwrap(); - - assert_eq!(identity1.name(), "alice"); - assert!(identity1 - .path() - .to_string_lossy() - .to_string() - .contains("alice.json")); - - // make sure that a named identity is not recreated twice - assert_eq!(identity1.name(), identity2.name()); - assert_eq!(identity1.path(), identity2.path()); - } - - #[tokio::test] - async fn migrate_legacy_cli_config() { - // Before this migration, there was a `config.json` file in the root $OCKAM_HOME directory - // that contained a map of space names to space and project lookups. This test ensures that - // the migration correctly moves the space and project lookups into the new `spaces` and - // `projects` directories, respectively. - let space_name = "sname"; - let space_lookup = SpaceLookup { - id: "sid".to_string(), - }; - let project_lookup = ProjectLookup { - node_route: Some(MultiAddr::from_str("/node/p").unwrap()), - id: "pid".to_string(), - name: "pname".to_string(), - identity_id: Some( - Identifier::from_str( - "Ibb37445cacb3ca7a20040a9b36469e321a57d2cda1b2c3d4e5f6a6b5c4d3e2f1", - ) - .unwrap(), - ), - authority: None, - okta: None, - }; - let test_dir = CliState::test_dir().unwrap(); - let legacy_config = { - let map = vec![ - (space_name.to_string(), LookupValue::Space(space_lookup)), - ( - project_lookup.name.clone(), - LookupValue::Project(project_lookup.clone()), - ), - ]; - let lookup = ConfigLookup { - map: map.into_iter().collect(), - }; - LegacyCliConfig { - dir: Some(test_dir.clone()), - lookup, - } - }; - std::fs::create_dir_all(&test_dir).unwrap(); - std::fs::write( - test_dir.join("config.json"), - serde_json::to_string(&legacy_config).unwrap(), - ) - .unwrap(); - let state = CliState::initialize_at(&test_dir).await.unwrap(); - let space = state.spaces.get(space_name).unwrap(); - assert_eq!(space.config().id, "sid"); - let project = state.projects.get(&project_lookup.name).unwrap(); - assert_eq!(project.config().id, project_lookup.id); - assert_eq!( - project.config().access_route, - project_lookup.node_route.unwrap().to_string() - ); - assert!(!test_dir.join("config.json").exists()); - } - - #[ockam_macros::test(crate = "ockam")] - async fn integration(ctx: &mut ockam::Context) -> ockam::Result<()> { - let sut = CliState::test()?; - - // Vaults - let vault_name = { - let name = random_name(); - let config = VaultConfig::default(); - - let state = sut.vaults.create_async(&name, config).await.unwrap(); - let got = sut.vaults.get(&name).unwrap(); - assert_eq!(got, state); - - let got = sut.vaults.default().unwrap(); - assert_eq!(got, state); - - name - }; - - // Identities - let identity_name = { - let name = random_name(); - let vault_state = sut.vaults.get(&vault_name).unwrap(); - let vault: Vault = vault_state.get().await.unwrap(); - let identities = Identities::builder() - .with_vault(vault) - .with_identities_repository(sut.identities.identities_repository().await?) - .build(); - let identifier = identities - .identities_creation() - .create_identity() - .await - .unwrap(); - let config = IdentityConfig::new(&identifier).await; - - let state = sut.identities.create(&name, config).unwrap(); - let got = sut.identities.get(&name).unwrap(); - assert_eq!(got, state); - - let got = sut.identities.default().unwrap(); - assert_eq!(got, state); - - name - }; - - // Nodes - let node_name = { - let name = random_name(); - let config = NodeConfig::try_from(&sut).unwrap(); - - let state = sut.nodes.create(&name, config).unwrap(); - let got = sut.nodes.get(&name).unwrap(); - assert_eq!(got, state); - - let got = sut.nodes.default().unwrap(); - assert_eq!(got, state); - - name - }; - - // Spaces - let space_name = { - let name = random_name(); - let id = random_string(); - let config = SpaceConfig { - name: name.clone(), - id, - }; - - let state = sut.spaces.create(&name, config).unwrap(); - let got = sut.spaces.get(&name).unwrap(); - assert_eq!(got, state); - - name - }; - - // Projects - let project_name = { - let name = random_name(); - let config = ProjectConfig::default(); - - let state = sut.projects.create(&name, config).unwrap(); - let got = sut.projects.get(&name).unwrap(); - assert_eq!(got, state); - - name - }; - - // Trust Contexts - let trust_context_name = { - let name = random_name(); - let config = TrustContextConfig::new(name.to_string(), None); - - let state = sut.trust_contexts.create(&name, config).unwrap(); - let got = sut.trust_contexts.get(&name).unwrap(); - assert_eq!(got, state); - - name - }; - - // Users Info - let user_info_email = { - let email = random_name(); - let config = UserInfo { - email: email.clone(), - ..Default::default() - }; - - let state = sut.users_info.create(&email, config).unwrap(); - let got = sut.users_info.get(&email).unwrap(); - assert_eq!(got, state); - - email - }; - - // Check structure - let mut expected_entries = vec![ - "vaults".to_string(), - format!("vaults/{vault_name}.json"), - "vaults/data".to_string(), - format!("vaults/data/{vault_name}-storage.json"), - "identities".to_string(), - format!("identities/{identity_name}.json"), - "identities/data/authenticated_storage.lmdb".to_string(), - "nodes".to_string(), - format!("nodes/{node_name}"), - "spaces".to_string(), - format!("spaces/{space_name}.json"), - "projects".to_string(), - format!("projects/{project_name}.json"), - "trust_contexts".to_string(), - format!("trust_contexts/{trust_context_name}.json"), - "users_info".to_string(), - format!("users_info/{user_info_email}.json"), - "credentials".to_string(), - "defaults".to_string(), - "defaults/vault".to_string(), - "defaults/identity".to_string(), - "defaults/node".to_string(), - "defaults/space".to_string(), - "defaults/project".to_string(), - "defaults/trust_context".to_string(), - "defaults/user_info".to_string(), - ]; - expected_entries.sort(); - let mut found_entries = vec![]; - sut.dir.read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let dir_name = entry.file_name().into_string().unwrap(); - match dir_name.as_str() { - "vaults" => { - assert!(entry.path().is_dir()); - found_entries.push(dir_name.clone()); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let entry_name = entry.file_name().into_string().unwrap(); - found_entries.push(format!("{dir_name}/{entry_name}")); - if entry.path().is_dir() { - assert_eq!(entry_name, DATA_DIR_NAME); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let file_name = entry.file_name().into_string().unwrap(); - if !file_name.ends_with(".lock") { - found_entries - .push(format!("{dir_name}/{entry_name}/{file_name}")); - assert_eq!(file_name, format!("{vault_name}-storage.json")); - } - }); - } else { - assert_eq!(entry_name, format!("{vault_name}.json")); - } - }); - } - "identities" => { - assert!(entry.path().is_dir()); - found_entries.push(dir_name.clone()); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let entry_name = entry.file_name().into_string().unwrap(); - if entry.path().is_dir() { - assert_eq!(entry_name, DATA_DIR_NAME); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let file_name = entry.file_name().into_string().unwrap(); - if !file_name.ends_with("-lock") { - found_entries - .push(format!("{dir_name}/{entry_name}/{file_name}")); - assert_eq!(file_name, format!("authenticated_storage.lmdb")); - } - }) - } else { - assert!(entry.path().is_file()); - let file_name = entry.file_name().into_string().unwrap(); - found_entries.push(format!("{dir_name}/{file_name}")); - } - }); - } - "nodes" => { - assert!(entry.path().is_dir()); - found_entries.push(dir_name.clone()); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - assert!(entry.path().is_dir()); - let file_name = entry.file_name().into_string().unwrap(); - found_entries.push(format!("{dir_name}/{file_name}")); - }); - } - "defaults" | "spaces" | "projects" | "credentials" | "trust_contexts" - | "users_info" => { - assert!(entry.path().is_dir()); - found_entries.push(dir_name.clone()); - entry.path().read_dir().unwrap().for_each(|entry| { - let entry = entry.unwrap(); - let entry_name = entry.file_name().into_string().unwrap(); - found_entries.push(format!("{dir_name}/{entry_name}")); - }); - } - _ => panic!("unexpected file"), - } - }); - found_entries.sort(); - assert_eq!(expected_entries, found_entries); - - sut.spaces.delete(&space_name).unwrap(); - sut.projects.delete(&project_name).unwrap(); - sut.nodes.delete(&node_name).unwrap(); - sut.identities.delete(&identity_name).unwrap(); - sut.vaults.delete(&vault_name).unwrap(); - - ctx.stop().await?; - Ok(()) - } -} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs b/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs index d8f2d97254d..758327264eb 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs @@ -1,89 +1,140 @@ -use super::Result; -use crate::cli_state::{ - CliState, CliStateError, IdentityConfig, IdentityState, ProjectConfig, ProjectConfigCompact, - StateDirTrait, StateItemTrait, VaultState, -}; -use crate::config::lookup::ProjectLookup; -use crate::nodes::models::transport::CreateTransportJson; -use backwards_compatibility::*; -use miette::{IntoDiagnostic, WrapErr}; +use std::path::PathBuf; +use std::process; + use nix::errno::Errno; -use ockam::identity::Identifier; -use ockam::identity::Vault; -use ockam::LmdbStorage; -use ockam_core::compat::collections::HashSet; -use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; use sysinfo::{Pid, ProcessExt, ProcessStatus, System, SystemExt}; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NodesState { - dir: PathBuf, -} - -impl NodesState { - pub fn stdout_logs(&self, name: &str) -> Result { - let dir = self.path(name); - std::fs::create_dir_all(&dir)?; - Ok(NodePaths::new(&dir).stdout()) - } +use ockam::identity::Identifier; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_multiaddr::MultiAddr; + +use crate::cli_state::{random_name, Result}; +use crate::cli_state::{CliState, CliStateError}; +use crate::cloud::project::Project; +use crate::config::lookup::InternetAddress; +use crate::NamedVault; + +/// The methods below support the creation and update of local nodes +/// +impl CliState { + /// Create a node, with some optional associated values: + /// + /// - an identity name. That identity is used by the `NodeManager` to create secure channels + /// - a project name. It is used to create policies on resources provisioned on a node (like a TCP outlet for example) + pub async fn create_node_with_optional_values( + &self, + node_name: &str, + identity_name: &Option, + project_name: &Option, + ) -> Result { + // Return the node if it has already been created + // and update its process id + if let Ok(node) = self.get_node(node_name).await { + self.set_node_pid(node_name, process::id()).await?; + return Ok(node); + }; - pub fn delete_sigkill(&self, name: &str, sigkill: bool) -> Result<()> { - self._delete(name, sigkill) + let identity = match identity_name { + Some(name) => self.get_named_identity(name).await?, + None => self.get_default_named_identity().await?, + }; + let node = self + .create_node_with_identifier(node_name, &identity.identifier()) + .await?; + self.set_node_project(node_name, project_name).await?; + Ok(node) + } + + /// This method creates a node with an associated identity + /// The vault used to create the identity is the default vault + pub async fn create_node(&self, node_name: &str) -> Result { + let identity = self.create_identity_with_name(&random_name()).await?; + self.create_node_with_identifier(node_name, &identity.identifier()) + .await + } + + /// Delete a node + /// - first stop it if it is running + /// - then remove it from persistent storage + pub async fn delete_node(&self, node_name: &str, force: bool) -> Result<()> { + self.stop_node(node_name, force).await?; + self.remove_node(node_name).await?; + Ok(()) } - fn _delete(&self, name: impl AsRef, sigkill: bool) -> Result<()> { - // If doesn't exist do nothing - if !self.exists(&name) { - return Ok(()); + /// Delete all created nodes + pub async fn delete_all_nodes(&self, force: bool) -> Result<()> { + let nodes = self.nodes_repository().await?.get_nodes().await?; + for node in nodes { + self.delete_node(&node.name(), force).await?; } - let node = self.get(&name)?; - // Set default to another node if it's the default - if self.is_default(&name)? { - // Remove link if it exists - let _ = std::fs::remove_file(self.default_path()?); - for node in self.list()? { - if node.name() != name.as_ref() && self.set_default(node.name()).is_ok() { - debug!(name=%node.name(), "set default node"); - break; - } - } - } - // Remove node directory - node.delete_sigkill(sigkill)?; Ok(()) } -} -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NodeState { - name: String, - path: PathBuf, - paths: NodePaths, - config: NodeConfig, -} + /// This method can be used to start a local node first + /// then create a project, and associate it to the node + pub async fn set_node_project( + &self, + node_name: &str, + project_name: &Option, + ) -> Result<()> { + let project = match project_name { + Some(name) => Some(self.get_project_by_name(name).await?), + None => self.get_default_project().await.ok(), + }; -impl NodeState { - fn _delete(&self, sikgill: bool) -> Result<()> { - self.kill_process(sikgill)?; - std::fs::remove_dir_all(&self.path)?; - let _ = std::fs::remove_file(self.path.with_extension("lock")); - let _ = std::fs::remove_dir(&self.path); // Make sure the dir is gone - info!(name=%self.name, "node deleted"); + if let Some(project) = project { + self.nodes_repository() + .await? + .set_node_project_name(node_name, &project.name()) + .await? + }; Ok(()) } - pub fn delete_sigkill(&self, sigkill: bool) -> Result<()> { - self._delete(sigkill) + /// Remove a node: + /// + /// - remove it from the repository + /// - remove the node log files + pub async fn remove_node(&self, node_name: &str) -> Result<()> { + // don't try to remove a node on a non-existent database + if !self.database_path().exists() { + return Ok(()); + }; + + // remove the node from the database + let repository = self.nodes_repository().await?; + let node_exists = repository.get_node(node_name).await.is_ok(); + repository.delete_node(node_name).await?; + // set another node as the default node + if node_exists { + let other_nodes = repository.get_nodes().await?; + if let Some(other_node) = other_nodes.first() { + repository.set_default_node(&other_node.name()).await?; + } + } + + // remove the node directory + let _ = std::fs::remove_dir_all(self.node_dir(node_name)); + debug!(name=%node_name, "node deleted"); + Ok(()) } - pub fn kill_process(&self, sigkill: bool) -> Result<()> { - if let Some(pid) = self.pid()? { + /// Stop a background node + /// + /// - if force is true, send a SIGKILL signal to the node process + pub async fn stop_node(&self, node_name: &str, force: bool) -> Result<()> { + let node = self.get_node(node_name).await?; + self.nodes_repository() + .await? + .set_no_node_pid(node_name) + .await?; + + if let Some(pid) = node.pid() { nix::sys::signal::kill( - nix::unistd::Pid::from_raw(pid), - if sigkill { + nix::unistd::Pid::from_raw(pid as i32), + if force { nix::sys::signal::Signal::SIGKILL } else { nix::sys::signal::Signal::SIGTERM @@ -91,7 +142,7 @@ impl NodeState { ) .or_else(|e| { if e == Errno::ESRCH { - tracing::warn!(node = %self.name(), %pid, "No such process"); + tracing::warn!(node = %node.name(), %pid, "No such process"); Ok(()) } else { Err(e) @@ -103,638 +154,428 @@ impl NodeState { format!("failed to stop PID `{pid}` with error `{e}`"), )) })?; - std::fs::remove_file(self.paths.pid())?; } - info!(name = %self.name(), "node process killed"); + info!(name = %node.name(), "node process killed"); Ok(()) } - pub fn set_setup(&self, setup: &NodeSetupConfig) -> Result<()> { - let contents = serde_json::to_string(setup)?; - std::fs::write(self.paths.setup(), contents)?; - info!(name = %self.name(), "setup config updated"); - Ok(()) + /// Set a node as the default node + pub async fn set_default_node(&self, node_name: &str) -> Result<()> { + Ok(self + .nodes_repository() + .await? + .set_default_node(node_name) + .await?) + } + + /// Set a TCP listener address on a node when the TCP listener has been started + pub async fn set_tcp_listener_address(&self, node_name: &str, address: String) -> Result<()> { + Ok(self + .nodes_repository() + .await? + .set_tcp_listener_address(node_name, address.as_str()) + .await?) + } + + /// Specify that a node is an authority node + /// This is used to display the node status since if the node TCP listener is not accessible + /// without a secure channel + pub async fn set_as_authority_node(&self, node_name: &str) -> Result<()> { + Ok(self + .nodes_repository() + .await? + .set_as_authority_node(node_name) + .await?) + } + + /// Set the current process id on a background node + /// Keeping track of a background node process id allows us to kill its process when stopping the node + pub async fn set_node_pid(&self, node_name: &str, pid: u32) -> Result<()> { + Ok(self + .nodes_repository() + .await? + .set_node_pid(node_name, pid) + .await?) } +} - pub fn pid(&self) -> Result> { - let path = self.paths.pid(); - if path.exists() { - let pid = std::fs::read_to_string(path)? - .parse::() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - Ok(Some(pid)) +/// The following methods return nodes data +impl CliState { + /// Return a node by name + pub async fn get_node(&self, node_name: &str) -> Result { + if let Some(node) = self.nodes_repository().await?.get_node(node_name).await? { + Ok(node) } else { - Ok(None) + Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("There is no node with name {node_name}"), + ) + .into()) } } - pub fn set_pid(&self, pid: i32) -> Result<()> { - std::fs::write(self.paths.pid(), pid.to_string())?; - Ok(()) + /// Return all the created nodes + pub async fn get_nodes(&self) -> Result> { + Ok(self.nodes_repository().await?.get_nodes().await?) } - pub fn is_running(&self) -> bool { - if let Ok(Some(pid)) = self.pid() { - let mut sys = System::new(); - sys.refresh_processes(); - if let Some(p) = sys.process(Pid::from(pid as usize)) { - // Under certain circumstances the process can be in a state where it's not running - // and we are unable to kill it. For example, `kill -9` a process created by - // `node create` in a Docker environment will result in a zombie process. - !matches!(p.status(), ProcessStatus::Dead | ProcessStatus::Zombie) - } else { - false - } + /// Return information about the default node (if there is one) + pub async fn get_default_node(&self) -> Result { + if let Some(node) = self.nodes_repository().await?.get_default_node().await? { + Ok(node) } else { - false + let identity = self.get_default_named_identity().await?; + let node = self + .create_node_with_identifier(&random_name(), &identity.identifier()) + .await?; + Ok(node) } } - pub fn stdout_log(&self) -> PathBuf { - self.paths.stdout() + /// Return the node information for the given node name, otherwise for the default node + pub async fn get_node_or_default(&self, node_name: &Option) -> Result { + match node_name { + Some(name) => self.get_node(name).await, + None => self.get_default_node().await, + } } - pub fn stderr_log(&self) -> PathBuf { - self.paths.stderr() + /// Return the project associated to a node if there is one + pub async fn get_node_project(&self, node_name: &str) -> Result { + match self + .nodes_repository() + .await? + .get_node_project_name(node_name) + .await? + { + Some(project_name) => self.get_project_by_name(&project_name).await, + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no project associated to node {node_name}"), + ) + .into()), + } } - pub async fn policies_storage(&self) -> Result { - Ok(LmdbStorage::new(self.paths.policies_storage()).await?) + /// Return the stdout log file used by a node + pub fn stdout_logs(&self, node_name: &str) -> Result { + Ok(self.create_node_dir(node_name)?.join("stdout.log")) } - pub fn name(&self) -> &str { - &self.name + /// Return the stderr log file used by a node + pub fn stderr_logs(&self, node_name: &str) -> Result { + Ok(self.create_node_dir(node_name)?.join("stderr.log")) } } -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct NodeConfig { - #[serde(flatten)] - setup: NodeSetupConfig, - #[serde(skip)] - version: ConfigVersion, - #[serde(skip)] - default_vault: PathBuf, - #[serde(skip)] - default_identity: PathBuf, -} - -impl NodeConfig { - pub fn new(cli_state: &CliState) -> Result { - Self::try_from(cli_state) - } - - pub fn setup(&self) -> &NodeSetupConfig { - &self.setup - } - - pub fn setup_mut(&self) -> NodeSetupConfig { - self.setup.clone() - } - - pub fn vault_path(&self) -> Result { - Ok(std::fs::canonicalize(&self.default_vault)?) +/// Private functions +impl CliState { + /// This method creates a node + pub async fn create_node_with_identifier( + &self, + node_name: &str, + identifier: &Identifier, + ) -> Result { + let repository = self.nodes_repository().await?; + let is_default = repository.is_default_node(node_name).await? + || repository.get_nodes().await?.is_empty(); + let tcp_listener_address = repository.get_tcp_listener_address(node_name).await?; + let node_info = NodeInfo::new( + node_name.to_string(), + identifier.clone(), + 0, + is_default, + false, + tcp_listener_address, + Some(process::id()), + ); + repository.store_node(&node_info).await?; + Ok(node_info) } - pub async fn vault(&self) -> Result { - let state = VaultState::load(self.vault_path()?)?; - state.get().await + /// Return the nodes using a given identity + pub(super) async fn get_nodes_by_identity_name( + &self, + identity_name: &str, + ) -> Result> { + let identifier = self.get_identifier_by_name(identity_name).await?; + Ok(self + .nodes_repository() + .await? + .get_nodes_by_identifier(&identifier) + .await?) } - pub fn identity_config(&self) -> Result { - let path = std::fs::canonicalize(&self.default_identity)?; - Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?) + /// Return the vault which was used to create the identity associated to a node + pub(super) async fn get_node_vault(&self, node_name: &str) -> Result { + let identifier = self.get_node(node_name).await?.identifier(); + let identity = self.get_named_identity_by_identifier(&identifier).await?; + self.get_named_vault(&identity.vault_name()).await } - pub fn identifier(&self) -> Result { - let state_path = std::fs::canonicalize(&self.default_identity)?; - let state = IdentityState::load(state_path)?; - Ok(state.identifier()) + /// Create a directory used to store files specific to a node + fn create_node_dir(&self, node_name: &str) -> Result { + let path = self.node_dir(node_name); + std::fs::create_dir_all(&path)?; + Ok(path) } -} -impl TryFrom<&CliState> for NodeConfig { - type Error = CliStateError; - - fn try_from(cli_state: &CliState) -> std::result::Result { - let default_vault = cli_state.vaults.default_path()?; - assert!(default_vault.exists(), "default vault does not exist"); - let default_identity = cli_state.identities.default_path()?; - assert!(default_identity.exists(), "default identity does not exist"); - Ok(Self { - version: ConfigVersion::latest(), - default_vault, - default_identity, - setup: NodeSetupConfig::default(), - }) + /// Return the directory used by a node + fn node_dir(&self, node_name: &str) -> PathBuf { + Self::make_node_dir_path(&self.dir(), node_name) } } -#[derive(Debug, Clone, Default)] -pub struct NodeConfigBuilder { - vault: Option, - identity: Option, -} - -impl NodeConfigBuilder { - pub fn vault(mut self, path: PathBuf) -> Self { - self.vault = Some(path); - self - } - - pub fn identity(mut self, path: PathBuf) -> Self { - self.identity = Some(path); - self - } - - pub fn build(self, cli_state: &CliState) -> Result { - let vault = match self.vault { - Some(path) => path, - None => cli_state.vaults.default_path()?, - }; - let identity = match self.identity { - Some(path) => path, - None => cli_state.identities.default_path()?, - }; - Ok(NodeConfig { - default_vault: vault, - default_identity: identity, - ..NodeConfig::new(cli_state)? - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum ConfigVersion { - V1, -} - -impl ConfigVersion { - fn latest() -> Self { - Self::V1 - } -} - -impl Display for ConfigVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - ConfigVersion::V1 => "1", - }) - } +/// This struct contains all the data associated to a node +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct NodeInfo { + name: String, + identifier: Identifier, + verbosity: u8, + // this is used when restarting the node to determine its logging level + is_default: bool, + is_authority: bool, + tcp_listener_address: Option, + pid: Option, } -impl FromStr for ConfigVersion { - type Err = CliStateError; - - fn from_str(s: &str) -> std::result::Result { - match s { - "1" => Ok(Self::V1), - _ => Err(CliStateError::InvalidVersion(s.to_string())), +impl NodeInfo { + pub fn new( + name: String, + identifier: Identifier, + verbosity: u8, + is_default: bool, + is_authority: bool, + tcp_listener_address: Option, + pid: Option, + ) -> Self { + Self { + name, + identifier, + verbosity, + is_default, + is_authority, + tcp_listener_address, + pid, } } -} - -impl Default for ConfigVersion { - fn default() -> Self { - Self::latest() - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] -pub struct NodeSetupConfig { - pub verbose: u8, - - /// This flag is used to determine how the node status should be - /// displayed in print_query_status. - /// The field might be missing in previous configuration files, hence it is an Option - pub authority_node: Option, - pub project: Option, - pub api_transport: Option, -} - -impl NodeSetupConfig { - pub fn set_verbose(mut self, verbose: u8) -> Self { - self.verbose = verbose; - self - } - - pub fn set_authority_node(mut self) -> Self { - self.authority_node = Some(true); - self + pub fn name(&self) -> String { + self.name.clone() } - pub fn set_project(&mut self, project: ProjectLookup) -> &mut Self { - self.project = Some(project); - self + pub fn identifier(&self) -> Identifier { + self.identifier.clone() } - pub fn set_api_transport(mut self, transport: CreateTransportJson) -> Self { - self.api_transport = Some(transport); - self + pub fn verbosity(&self) -> u8 { + self.verbosity } - pub fn api_transport(&self) -> Result<&CreateTransportJson> { - self.api_transport.as_ref().ok_or_else(|| { - CliStateError::InvalidOperation( - "The api transport was not set for the node".to_string(), - ) - }) + pub fn is_default(&self) -> bool { + self.is_default } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -struct NodePaths { - path: PathBuf, -} -impl NodePaths { - fn new(path: &Path) -> Self { - Self { - path: path.to_path_buf(), - } + /// Return a copy of this node with the is_default flag set to true + pub fn set_as_default(&self) -> Self { + let mut result = self.clone(); + result.is_default = true; + result } - fn setup(&self) -> PathBuf { - self.path.join("setup.json") + pub fn is_authority_node(&self) -> bool { + self.is_authority } - fn vault(&self) -> PathBuf { - self.path.join("default_vault") + pub fn tcp_listener_port(&self) -> Option { + self.tcp_listener_address.as_ref().map(|t| t.port()) } - fn identity(&self) -> PathBuf { - self.path.join("default_identity") + pub fn tcp_listener_address(&self) -> Option { + self.tcp_listener_address.clone() } - fn pid(&self) -> PathBuf { - self.path.join("pid") + pub fn tcp_listener_multi_address(&self) -> Result { + Ok(self + .tcp_listener_address + .as_ref() + .ok_or(ockam::Error::new( + Origin::Api, + Kind::Internal, + "no transport has been set on the node".to_string(), + )) + .and_then(|t| t.multi_addr())?) } - fn version(&self) -> PathBuf { - self.path.join("version") + pub fn pid(&self) -> Option { + self.pid } - fn stdout(&self) -> PathBuf { - self.path.join("stdout.log") + pub fn set_pid(&self, pid: u32) -> NodeInfo { + let mut result = self.clone(); + result.pid = Some(pid); + result } - fn stderr(&self) -> PathBuf { - self.path.join("stderr.log") - } - - fn policies_storage(&self) -> PathBuf { - self.path.join("policies_storage.lmdb") + /// Return true if there is a running process corresponding to the node process id + pub fn is_running(&self) -> bool { + if let Some(pid) = self.pid() { + let mut sys = System::new(); + sys.refresh_processes(); + if let Some(p) = sys.process(Pid::from(pid as usize)) { + // Under certain circumstances the process can be in a state where it's not running + // and we are unable to kill it. For example, `kill -9` a process created by + // `node create` in a Docker environment will result in a zombie process. + !matches!(p.status(), ProcessStatus::Dead | ProcessStatus::Zombie) + } else { + false + } + } else { + false + } } } -mod backwards_compatibility { - use super::*; +#[cfg(test)] +mod tests { + use ockam_core::env::FromString; - #[derive(Deserialize, Debug, Clone)] - #[serde(untagged)] - pub(super) enum NodeConfigs { - V1(NodeConfigV1), - V2(NodeConfig), - } - - #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] - pub(super) struct NodeConfigV1 { - #[serde(flatten)] - pub setup: NodeSetupConfigV1, - #[serde(skip)] - pub version: ConfigVersion, - #[serde(skip)] - pub default_vault: PathBuf, - #[serde(skip)] - pub default_identity: PathBuf, - } - - #[derive(Deserialize, Debug, Clone)] - #[serde(untagged)] - pub(super) enum NodeSetupConfigs { - V1(NodeSetupConfigV1), - V2(NodeSetupConfig), - } - - // The change was replacing the `transports` field with `api_transport` - #[derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] - pub(super) struct NodeSetupConfigV1 { - pub verbose: u8, - - /// This flag is used to determine how the node status should be - /// displayed in print_query_status. - /// The field might be missing in previous configuration files, hence it is an Option - pub authority_node: Option, - pub project: Option, - pub transports: HashSet, - } - - #[cfg(test)] - impl NodeSetupConfigV1 { - pub fn add_transport(mut self, transport: CreateTransportJson) -> Self { - self.transports.insert(transport); - self - } - } -} + use crate::config::lookup::InternetAddress; -mod traits { use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use crate::nodes::models::transport::{TransportMode, TransportType}; - use ockam_core::async_trait; - - #[async_trait] - impl StateDirTrait for NodesState { - type Item = NodeState; - const DEFAULT_FILENAME: &'static str = "node"; - const DIR_NAME: &'static str = "nodes"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } - fn dir(&self) -> &PathBuf { - &self.dir - } + #[tokio::test] + async fn test_create_node() -> Result<()> { + let cli = CliState::test().await?; - fn path(&self, name: impl AsRef) -> PathBuf { - self.dir().join(name.as_ref()) - } + // a node can be created with just a name + let node_name = "node-1"; + let result = cli.create_node(node_name).await?; + assert_eq!(result.name(), node_name.to_string()); - /// A node contains several files, and the existence of the main directory is not not enough - /// to determine if a node exists as it could be created but empty. - fn exists(&self, name: impl AsRef) -> bool { - let paths = NodePaths::new(&self.path(&name)); - paths.setup().exists() - } + // the first node is the default one + let result = cli.get_default_node().await?.name(); + assert_eq!(result, node_name.to_string()); - fn delete(&self, name: impl AsRef) -> Result<()> { - self._delete(&name, false) - } + // as a consequence, a default identity must have been created + let result = cli.get_default_named_vault().await.ok(); + assert!(result.is_some()); - async fn migrate(&self, node_path: &Path) -> Result<()> { - if node_path.is_file() { - // If path is a file, it is probably a non supported file (e.g. .DS_Store) - return Ok(()); - } - let paths = NodePaths::new(node_path); - let contents = std::fs::read_to_string(paths.setup())?; - match serde_json::from_str(&contents)? { - NodeSetupConfigs::V1(setup) => { - // Get the first tcp-listener from the transports hashmap and - // use it as the api transport - let mut new_setup = NodeSetupConfig { - verbose: setup.verbose, - authority_node: setup.authority_node, - project: setup.project, - api_transport: None, - }; - if let Some(t) = setup - .transports - .into_iter() - .find(|t| t.tt == TransportType::Tcp && t.tm == TransportMode::Listen) - { - new_setup.api_transport = Some(t); - } - std::fs::write(paths.setup(), serde_json::to_string(&new_setup)?)?; - } - NodeSetupConfigs::V2(_) => (), - } - Ok(()) - } + let result = cli.get_default_named_identity().await.ok(); + assert!(result.is_some()); + + // that identity is associated to the node + let identifier = result.unwrap().identifier(); + let result = cli.get_node(node_name).await?.identifier(); + assert_eq!(result, identifier); + Ok(()) } - #[async_trait] - impl StateItemTrait for NodeState { - type Config = NodeConfig; - - fn new(path: PathBuf, mut config: Self::Config) -> Result { - std::fs::create_dir_all(&path)?; - let paths = NodePaths::new(&path); - let name = file_stem(&path)?; - std::fs::write(paths.setup(), serde_json::to_string(config.setup())?)?; - std::fs::write(paths.version(), config.version.to_string())?; - let _ = std::fs::remove_file(paths.vault()); - std::os::unix::fs::symlink(&config.default_vault, paths.vault())?; - config.default_vault = paths.vault(); - let _ = std::fs::remove_file(paths.identity()); - std::os::unix::fs::symlink(&config.default_identity, paths.identity())?; - config.default_identity = paths.identity(); - Ok(Self { - name, - path, - paths, - config, - }) - } + #[tokio::test] + async fn test_update_node() -> Result<()> { + let cli = CliState::test().await?; - fn load(path: PathBuf) -> Result { - let paths = NodePaths::new(&path); - let name = file_stem(&path)?; - let setup = { - let contents = std::fs::read_to_string(paths.setup())?; - serde_json::from_str(&contents)? - }; - let version = { - let contents = std::fs::read_to_string(paths.version())?; - contents.parse::()? - }; - let config = NodeConfig { - setup, - version, - default_vault: paths.vault(), - default_identity: paths.identity(), - }; - Ok(Self { - name, - path, - paths, - config, - }) - } + // create a node + let node_name = "node-1"; + let _ = cli.create_node(node_name).await?; + cli.set_tcp_listener_address(node_name, "127.0.0.1:0".to_string()) + .await?; - fn delete(&self) -> Result<()> { - self._delete(false) - } + // recreate the node with the same name + let _ = cli.create_node(node_name).await?; - fn path(&self) -> &PathBuf { - &self.path - } + // the node must still be the default node + let result = cli.get_default_node().await?; + assert_eq!(result.name(), node_name.to_string()); + assert!(result.is_default()); - fn config(&self) -> &Self::Config { - &self.config - } + // the original tcp listener address has been kept + assert_eq!( + result.tcp_listener_address(), + InternetAddress::new("127.0.0.1:0") + ); + Ok(()) } -} -pub async fn init_node_state( - cli_state: &CliState, - node_name: &str, - vault_name: Option<&str>, - identity_name: Option<&str>, -) -> miette::Result<()> { - debug!(name=%node_name, "initializing node state"); - // Get vault specified in the argument, or get the default - let vault_state = cli_state.create_vault_state(vault_name).await?; - - // create an identity for the node - let identifier = cli_state - .get_identities(vault_state.get().await?) - .await? - .identities_creation() - .create_identity() - .await - .into_diagnostic() - .wrap_err("Failed to create identity")?; - - let identity_state = cli_state - .create_identity_state(&identifier, identity_name) - .await?; - - // Create the node with the given vault and identity - let node_config = NodeConfigBuilder::default() - .vault(vault_state.path().clone()) - .identity(identity_state.path().clone()) - .build(cli_state)?; - cli_state.nodes.overwrite(node_name, node_config)?; - - info!(name=%node_name, "node state initialized"); - Ok(()) -} + #[tokio::test] + async fn test_remove_node() -> Result<()> { + let cli = CliState::test().await?; -pub async fn add_project_info_to_node_state( - node_name: &str, - cli_state: &CliState, - project_path: Option<&PathBuf>, -) -> Result> { - debug!(name=%node_name, "Adding project info to state"); - let proj_path = if let Some(path) = project_path { - Some(path.clone()) - } else if let Ok(proj) = cli_state.projects.default() { - Some(proj.path().clone()) - } else { - None - }; - - match proj_path { - Some(path) => { - debug!(path=%path.display(), "Reading project info from path"); - let s = std::fs::read_to_string(path)?; - let proj_info: ProjectConfigCompact = serde_json::from_str(&s)?; - let proj_lookup = ProjectLookup::from_project(&(&proj_info).into()) - .await - .map_err(|e| { - CliStateError::InvalidData(format!("Failed to read project: {}", e)) - })?; - let proj_config = ProjectConfig::from(&proj_info); - let state = cli_state.nodes.get(node_name)?; - state.set_setup(state.config().setup_mut().set_project(proj_lookup.clone()))?; - cli_state - .projects - .overwrite(proj_lookup.name, proj_config)?; - Ok(Some(proj_lookup.id)) - } - None => { - debug!("No project info used"); - Ok(None) - } - } -} + // a node can be created with just a name + let node1 = "node-1"; + let node_info1 = cli.create_node(node1).await?; -pub async fn update_enrolled_identity(cli_state: &CliState, node_name: &str) -> Result { - let identities = cli_state.identities.list()?; + // the created node is set as the default node + let result = cli.get_default_node().await?; + assert_eq!(result, node_info1); - let node_state = cli_state.nodes.get(node_name)?; - let node_identifier = node_state.config().identifier()?; + // a node can also be removed + // first let's create a second node + let node2 = "node-2"; + let node_info2 = cli.create_node(node2).await?; - for mut identity in identities { - if node_identifier == identity.config().identifier() { - identity.set_enrollment_status()?; - } - } + // and remove node 1 + cli.remove_node(node1).await?; - Ok(node_identifier) -} + let result = cli.get_node(node1).await.ok(); + assert_eq!( + result, None, + "the node information is not available anymore" + ); + assert!( + !cli.node_dir(node1).exists(), + "the node directory must be deleted" + ); -#[cfg(test)] -mod tests { - use super::*; - use crate::config::lookup::InternetAddress; - use crate::nodes::models::transport::{TransportMode, TransportType}; - - #[test] - fn node_config_setup_transports_no_duplicates() { - let mut config = NodeSetupConfigV1 { - verbose: 0, - authority_node: None, - project: None, - transports: HashSet::new(), - }; - let transport = CreateTransportJson { - tt: TransportType::Tcp, - tm: TransportMode::Listen, - addr: InternetAddress::V4("127.0.0.1:1020".parse().unwrap()), - }; - config = config.add_transport(transport.clone()); - assert_eq!(config.transports.len(), 1); - assert_eq!(config.transports.iter().next(), Some(&transport)); - - config = config.add_transport(transport); - assert_eq!(config.transports.len(), 1); - } - - #[test] - fn node_config_setup_transports_parses_a_json_with_duplicate_entries() { - // This test is to ensure backwards compatibility, for versions where transports where stored as a Vec<> - let config_json = r#"{ - "verbose": 0, - "authority_node": null, - "project": null, - "transports": [ - {"tt":"Tcp","tm":"Listen","addr":{"V4":"127.0.0.1:1020"}}, - {"tt":"Tcp","tm":"Listen","addr":{"V4":"127.0.0.1:1020"}} - ] - }"#; - let config = serde_json::from_str::(config_json).unwrap(); - assert_eq!(config.transports.len(), 1); + // then node 2 should be the default node + let result = cli.get_default_node().await?; + assert_eq!(result, node_info2.set_as_default()); + Ok(()) } #[tokio::test] - async fn migrate_node_config_from_v1_to_v2() { - // Create a v1 setup.json file - let v1_json_json = r#"{ - "verbose": 0, - "authority_node": null, - "project": null, - "transports": [ - {"tt":"Tcp","tm":"Listen","addr":{"V4":"127.0.0.1:1020"}} - ] - }"#; - let tmp_dir = tempfile::tempdir().unwrap(); - let node_dir = tmp_dir.path().join("n"); - std::fs::create_dir(&node_dir).unwrap(); - let tmp_file = node_dir.join("setup.json"); - std::fs::write(&tmp_file, v1_json_json).unwrap(); - - // Run migration - let nodes_state = NodesState::new(tmp_dir.path()); - nodes_state.migrate(&node_dir).await.unwrap(); - - // Check migration was done correctly - let contents = std::fs::read_to_string(&tmp_file).unwrap(); - let v2_setup: NodeSetupConfig = serde_json::from_str(&contents).unwrap(); - assert_eq!( - v2_setup.api_transport, - Some(CreateTransportJson { - tt: TransportType::Tcp, - tm: TransportMode::Listen, - addr: InternetAddress::V4("127.0.0.1:1020".parse().unwrap()) - }) - ); + async fn test_create_node_with_optional_values() -> Result<()> { + let cli = CliState::test().await?; + + // a node can be created with just a name + let node = cli + .create_node_with_optional_values("node-1", &None, &None) + .await?; + let result = cli.get_node(&node.name()).await?; + assert_eq!(result.name(), node.name()); + + // a node can be created with a name and an existing identity + let identity = cli.create_identity_with_name("name").await?; + let node = cli + .create_node_with_optional_values("node-2", &Some(identity.name()), &None) + .await?; + let result = cli.get_node(&node.name()).await?; + assert_eq!(result.identifier(), identity.identifier()); + + // a node can be created with a name, an existing identity and an existing project + let authority = cli.get_identity(&identity.identifier()).await?; + let project = cli + .import_project( + "project_id", + "project_name", + &None, + &MultiAddr::from_string("/project/default").unwrap(), + &Some(authority), + &Some(MultiAddr::from_string("/project/authority").unwrap()), + ) + .await?; + + let node = cli + .create_node_with_optional_values( + "node-4", + &Some(identity.name()), + &Some(project.project_name()), + ) + .await?; + let result = cli.get_node_project(&node.name()).await?; + assert_eq!(result.project_name(), project.project_name()); + + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/policies.rs b/implementations/rust/ockam/ockam_api/src/cli_state/policies.rs new file mode 100644 index 00000000000..873e60feb64 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/policies.rs @@ -0,0 +1,54 @@ +use crate::cli_state::CliState; +use crate::cli_state::Result; +use ockam_abac::{Action, Env, Expr, PolicyAccessControl, Resource}; + +impl CliState { + pub async fn get_policy(&self, r: &Resource, a: &Action) -> Result> { + Ok(self.policies_repository().await?.get_policy(r, a).await?) + } + + pub async fn set_policy(&self, r: &Resource, a: &Action, c: &Expr) -> Result<()> { + Ok(self + .policies_repository() + .await? + .set_policy(r, a, c) + .await?) + } + + pub async fn delete_policy(&self, r: &Resource, a: &Action) -> Result<()> { + Ok(self + .policies_repository() + .await? + .delete_policy(r, a) + .await?) + } + + pub async fn get_policies_by_resource(&self, r: &Resource) -> Result> { + Ok(self + .policies_repository() + .await? + .get_policies_by_resource(r) + .await?) + } + + pub async fn make_policy_access_control( + &self, + r: &Resource, + a: &Action, + env: Env, + ) -> Result { + let policies = self.policies_repository().await?.clone(); + debug!( + "set a policy access control for resource '{}' and action '{}'", + &r, &a + ); + + Ok(PolicyAccessControl::new( + policies, + self.identity_attributes_repository().await?, + r.clone(), + a.clone(), + env, + )) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs b/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs index 00a81869f00..75e0ffebddb 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs @@ -1,171 +1,154 @@ -use super::Result; -use crate::cloud::project::{OktaConfig, Project}; -use crate::config::lookup::ProjectLookup; -use crate::error::ApiError; -use ockam::identity::Identifier; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ProjectsState { - dir: PathBuf, -} +use std::collections::HashMap; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ProjectState { - name: String, - path: PathBuf, - config: ProjectConfig, -} +use ockam::identity::{Identifier, Identity}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_multiaddr::MultiAddr; -impl ProjectState { - pub fn id(&self) -> &str { - &self.config.id - } - pub fn name(&self) -> &str { - &self.name - } -} +use crate::cli_state::CliState; +use crate::cloud::project::Project; -pub type ProjectConfig = Project; +use super::Result; -impl From for Project { - fn from(lookup: ProjectLookup) -> Self { - Self { - id: lookup.id, - name: lookup.name, +impl CliState { + pub async fn import_project( + &self, + project_id: &str, + project_name: &str, + project_identifier: &Option, + project_access_route: &MultiAddr, + authority_identity: &Option, + authority_access_route: &Option, + ) -> Result { + let authority_identity = match authority_identity { + Some(identity) => Some(identity.change_history().export_as_string()?), + None => None, + }; + let project = Project { + id: project_id.to_string(), + name: project_name.to_string(), space_name: "".to_string(), - access_route: lookup - .node_route - .map(|r| r.to_string()) - .unwrap_or("".to_string()), + access_route: project_access_route.to_string(), users: vec![], space_id: "".to_string(), - identity: lookup.identity_id, - authority_access_route: lookup.authority.as_ref().map(|a| a.address().to_string()), - authority_identity: lookup.authority.as_ref().map(|a| hex::encode(a.identity())), - okta_config: lookup.okta.map(|o| o.into()), + identity: project_identifier.clone(), + authority_access_route: authority_access_route.clone().map(|r| r.to_string()), + authority_identity, + okta_config: None, confluent_config: None, version: None, running: None, operation_id: None, user_roles: vec![], - } + }; + self.store_project(project.clone()).await?; + Ok(project) } -} -#[derive(Debug, Clone, Serialize, Deserialize)] -#[non_exhaustive] -pub struct ProjectConfigCompact { - pub id: String, - pub name: String, - pub identity: Option, - pub access_route: String, - pub authority_access_route: Option, - pub authority_identity: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub okta_config: Option, -} - -impl TryFrom for ProjectConfigCompact { - type Error = ApiError; - fn try_from(p: ProjectLookup) -> core::result::Result { - Ok(Self { - id: p.id, - name: p.name, - identity: p.identity_id, - access_route: p - .node_route - .map_or( - Err(ApiError::message("Project access route is missing")), - Ok, - )? - .to_string(), - authority_access_route: p.authority.as_ref().map(|a| a.address().to_string()), - authority_identity: p.authority.as_ref().map(|a| hex::encode(a.identity())), - okta_config: p.okta.map(|o| o.into()), - }) + pub async fn store_project(&self, project: Project) -> Result<()> { + let repository = self.projects_repository().await?; + repository.store_project(&project).await?; + // If there is no previous default project set this project as the default + let default_project = repository.get_default_project().await?; + if default_project.is_none() { + repository.set_default_project(&project.id).await? + }; + + // create a corresponding trust context + self.create_trust_context( + Some(project.name()), + Some(project.id()), + None, + Some(project.authority_identity().await?), + Some(project.authority_access_route()?), + ) + .await?; + Ok(()) } -} -impl From for ProjectConfigCompact { - fn from(p: Project) -> Self { - Self { - id: p.id, - name: p.name, - identity: p.identity, - access_route: p.access_route, - authority_access_route: p.authority_access_route, - authority_identity: p.authority_identity, - okta_config: p.okta_config, - } - } -} - -impl From<&ProjectConfigCompact> for Project { - fn from(p: &ProjectConfigCompact) -> Self { - Project { - id: p.id.to_string(), - name: p.name.to_string(), - identity: p.identity.to_owned(), - access_route: p.access_route.to_string(), - authority_access_route: p.authority_access_route.as_ref().map(|a| a.to_string()), - authority_identity: p.authority_identity.as_ref().map(|a| a.to_string()), - okta_config: p.okta_config.clone(), - ..Default::default() + pub async fn delete_project(&self, project_id: &str) -> Result<()> { + let repository = self.projects_repository().await?; + // delete the project + let project_exists = repository.get_project(project_id).await.is_ok(); + repository.delete_project(project_id).await?; + + // set another project as the default project + if project_exists { + let other_projects = repository.get_projects().await?; + if let Some(other_project) = other_projects.first() { + repository.set_default_project(&other_project.id()).await?; + } } + Ok(()) } -} - -mod traits { - use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for ProjectsState { - type Item = ProjectState; - const DEFAULT_FILENAME: &'static str = "project"; - const DIR_NAME: &'static str = "projects"; - const HAS_DATA_DIR: bool = false; - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), + pub async fn get_default_project(&self) -> Result { + match self + .projects_repository() + .await? + .get_default_project() + .await? + { + Some(project) => Ok(project), + None => { + Err(Error::new(Origin::Api, Kind::NotFound, "there is no default project").into()) } } + } - fn dir(&self) -> &PathBuf { - &self.dir + pub async fn get_project_by_name(&self, name: &str) -> Result { + match self + .projects_repository() + .await? + .get_project_by_name(name) + .await? + { + Some(project) => Ok(project), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no project named {name}"), + ) + .into()), } } - #[async_trait] - impl StateItemTrait for ProjectState { - type Config = ProjectConfig; - - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - Ok(Self { name, path, config }) + pub async fn get_project(&self, project_id: &str) -> Result { + match self + .projects_repository() + .await? + .get_project(project_id) + .await? + { + Some(project) => Ok(project), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no space project with id {project_id}"), + ) + .into()), } + } - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { name, path, config }) + pub async fn get_project_by_name_or_default( + &self, + project_name: &Option, + ) -> Result { + match project_name { + Some(project_name) => self.get_project_by_name(project_name.as_str()).await, + None => self.get_default_project().await, } + } - fn path(&self) -> &PathBuf { - &self.path - } + pub async fn get_projects(&self) -> Result> { + Ok(self.projects_repository().await?.get_projects().await?) + } - fn config(&self) -> &Self::Config { - &self.config + pub async fn get_projects_grouped_by_name(&self) -> Result> { + let mut projects = HashMap::new(); + for project in self.get_projects().await? { + projects.insert(project.name.clone(), project); } + Ok(projects) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/repositories.rs b/implementations/rust/ockam/ockam_api/src/cli_state/repositories.rs new file mode 100644 index 00000000000..4993d45d44a --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/repositories.rs @@ -0,0 +1,80 @@ +use ockam::identity::storage::{PurposeKeysRepository, PurposeKeysSqlxDatabase}; +use ockam::identity::{ + ChangeHistoryRepository, ChangeHistorySqlxDatabase, IdentityAttributesRepository, + IdentityAttributesSqlxDatabase, +}; +use ockam_abac::{PoliciesRepository, PolicySqlxDatabase}; +use ockam_core::compat::sync::Arc; + +use crate::cli_state::error::Result; +use crate::cli_state::storage::*; +use crate::cli_state::CliState; +use crate::cli_state::{EnrollmentsRepository, EnrollmentsSqlxDatabase}; +use crate::cli_state::{ProjectsRepository, ProjectsSqlxDatabase}; +use crate::cli_state::{SpacesRepository, SpacesSqlxDatabase}; +use crate::cli_state::{TrustContextsRepository, TrustContextsSqlxDatabase}; +use crate::cli_state::{UsersRepository, UsersSqlxDatabase}; + +/// These functions create repository implementations to access data +/// stored in the database +impl CliState { + pub(super) async fn change_history_repository( + &self, + ) -> Result> { + Ok(Arc::new(ChangeHistorySqlxDatabase::new(self.database()))) + } + + pub(super) async fn identity_attributes_repository( + &self, + ) -> Result> { + Ok(Arc::new(IdentityAttributesSqlxDatabase::new( + self.database(), + ))) + } + + pub(super) async fn identities_repository(&self) -> Result> { + Ok(Arc::new(IdentitiesSqlxDatabase::new(self.database()))) + } + + pub(super) async fn purpose_keys_repository(&self) -> Result> { + Ok(Arc::new(PurposeKeysSqlxDatabase::new(self.database()))) + } + + pub(super) async fn vaults_repository(&self) -> Result> { + Ok(Arc::new(VaultsSqlxDatabase::new(self.database()))) + } + + pub(super) async fn enrollment_repository(&self) -> Result> { + Ok(Arc::new(EnrollmentsSqlxDatabase::new(self.database()))) + } + + pub(super) async fn nodes_repository(&self) -> Result> { + Ok(Arc::new(NodesSqlxDatabase::new(self.database()))) + } + + pub(super) async fn policies_repository(&self) -> Result> { + Ok(Arc::new(PolicySqlxDatabase::new(self.database()))) + } + + pub(super) async fn projects_repository(&self) -> Result> { + Ok(Arc::new(ProjectsSqlxDatabase::new(self.database()))) + } + + pub(super) async fn spaces_repository(&self) -> Result> { + Ok(Arc::new(SpacesSqlxDatabase::new(self.database()))) + } + + pub(super) async fn users_repository(&self) -> Result> { + Ok(Arc::new(UsersSqlxDatabase::new(self.database()))) + } + + pub(super) async fn credentials_repository(&self) -> Result> { + Ok(Arc::new(CredentialsSqlxDatabase::new(self.database()))) + } + + pub(super) async fn trust_contexts_repository( + &self, + ) -> Result> { + Ok(Arc::new(TrustContextsSqlxDatabase::new(self.database()))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs b/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs new file mode 100644 index 00000000000..45b7db46348 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/secure_channels.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use ockam::identity::{ChangeHistoryRepository, IdentityAttributesRepository, SecureChannels}; + +use crate::bootstrapped_identities_store::{ + BootstrapedIdentityAttributesStore, PreTrustedIdentities, +}; +use crate::cli_state::CliState; +use crate::cli_state::Result; + +impl CliState { + pub async fn secure_channels( + &self, + node_name: &str, + pre_trusted_identities: Option, + ) -> Result> { + let change_history_repository: Arc = + self.change_history_repository().await?; + let identity_attributes_repository: Arc = + self.identity_attributes_repository().await?; + + //TODO: fix this. Either don't require it to be a bootstrappedidentitystore (and use the + //trait instead), or pass it from the general_options always. + let vault = self.get_node_vault(node_name).await?.vault().await?; + let identity_attributes_repository: Arc = + Arc::new(match pre_trusted_identities { + None => BootstrapedIdentityAttributesStore::new( + Arc::new(PreTrustedIdentities::new_from_string("{}")?), + identity_attributes_repository.clone(), + ), + Some(f) => BootstrapedIdentityAttributesStore::new( + Arc::new(f), + identity_attributes_repository.clone(), + ), + }); + + debug!("create the secure channels service"); + let secure_channels = SecureChannels::builder() + .await? + .with_vault(vault) + .with_change_history_repository(change_history_repository.clone()) + .with_identity_attributes_repository(identity_attributes_repository.clone()) + .with_purpose_keys_repository(self.purpose_keys_repository().await?) + .build(); + Ok(secure_channels) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs b/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs index 8d3d8d6ce44..b5538422664 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs @@ -1,100 +1,113 @@ -use super::Result; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; + +use crate::cli_state::CliState; use crate::cloud::space::Space; -use crate::config::lookup::SpaceLookup; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct SpacesState { - dir: PathBuf, -} +use super::Result; -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct SpaceState { - name: String, - path: PathBuf, - config: SpaceConfig, -} +impl CliState { + pub async fn store_space( + &self, + space_id: &str, + space_name: &str, + users: Vec<&str>, + ) -> Result { + let repository = self.spaces_repository().await?; + let space = Space { + id: space_id.to_string(), + name: space_name.to_string(), + users: users.iter().map(|u| u.to_string()).collect(), + }; -impl SpaceState { - pub fn name(&self) -> &str { - &self.name - } -} + repository.store_space(&space).await?; -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct SpaceConfig { - pub name: String, - pub id: String, -} + // If there is no previous default space set this space as the default + let default_space = repository.get_default_space().await?; + if default_space.is_none() { + repository.set_default_space(&space.id).await? + }; + + Ok(space) + } -impl SpaceConfig { - pub fn from_lookup(name: &str, lookup: SpaceLookup) -> Self { - Self { - name: name.to_string(), - id: lookup.id, + pub async fn get_default_space(&self) -> Result { + match self.spaces_repository().await?.get_default_space().await? { + Some(space) => Ok(space), + None => { + Err(Error::new(Origin::Api, Kind::NotFound, "there is no default space").into()) + } } } -} -impl From<&Space> for SpaceConfig { - fn from(s: &Space) -> Self { - Self { - name: s.name.to_string(), - id: s.id.to_string(), + pub async fn get_space_by_name(&self, name: &str) -> Result { + match self + .spaces_repository() + .await? + .get_space_by_name(name) + .await? + { + Some(space) => Ok(space), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no space with name {name}"), + ) + .into()), } } -} -mod traits { - use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for SpacesState { - type Item = SpaceState; - const DEFAULT_FILENAME: &'static str = "space"; - const DIR_NAME: &'static str = "spaces"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), + pub async fn get_spaces(&self) -> Result> { + Ok(self.spaces_repository().await?.get_spaces().await?) + } + + pub async fn delete_space(&self, space_id: &str) -> Result<()> { + let repository = self.spaces_repository().await?; + // delete the space + let space_exists = repository.get_space(space_id).await.is_ok(); + repository.delete_space(space_id).await?; + + // set another space as the default space + if space_exists { + let other_space = repository.get_spaces().await?; + if let Some(other_space) = other_space.first() { + repository + .set_default_space(&other_space.space_id()) + .await?; } } + Ok(()) + } - fn dir(&self) -> &PathBuf { - &self.dir - } + pub async fn set_space_as_default(&self, space_id: &str) -> Result<()> { + Ok(self + .spaces_repository() + .await? + .set_default_space(space_id) + .await?) } +} - #[async_trait] - impl StateItemTrait for SpaceState { - type Config = SpaceConfig; +#[cfg(test)] +mod test { + use super::*; - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - Ok(Self { name, path, config }) - } + #[tokio::test] + async fn test_cli_spaces() -> Result<()> { + let cli = CliState::test().await?; - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { name, path, config }) - } + // the first created space becomes the default + let space1 = cli + .store_space("1", "name1", vec!["me@ockam.io", "you@ockam.io"]) + .await?; + let result = cli.get_default_space().await?; + assert_eq!(result, space1); - fn path(&self) -> &PathBuf { - &self.path - } + // the store method can be used to update a space + let updated_space1 = cli.store_space("1", "name1", vec!["them@ockam.io"]).await?; + let result = cli.get_default_space().await?; + assert_eq!(result, updated_space1); - fn config(&self) -> &Self::Config { - &self.config - } + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/credentials_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/credentials_repository.rs new file mode 100644 index 00000000000..23a9478b677 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/credentials_repository.rs @@ -0,0 +1,26 @@ +use crate::cli_state::NamedCredential; +use ockam::identity::models::CredentialAndPurposeKey; +use ockam::identity::Identity; +use ockam_core::async_trait; +use ockam_core::Result; + +/// This repository support the storage of credentials retrieved from the command line +/// A credential is associated with a name and its issuer for later retrieval +#[async_trait] +pub trait CredentialsRepository: Send + Sync + 'static { + /// Store a CredentialAndPurposeKey under a given name + /// The issuer of the credential is also stored in order to be able to validate the credential + /// later on + async fn store_credential( + &self, + name: &str, + issuer: &Identity, + credential: CredentialAndPurposeKey, + ) -> Result; + + /// Retrieve a credential given its name + async fn get_credential(&self, name: &str) -> Result>; + + /// Retrieve all the stored credentials + async fn get_credentials(&self) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/credentials_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/credentials_repository_sql.rs new file mode 100644 index 00000000000..bbb46a7b967 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/credentials_repository_sql.rs @@ -0,0 +1,168 @@ +use std::sync::Arc; + +use sqlx::*; + +use ockam::identity::models::{ChangeHistory, CredentialAndPurposeKey}; +use ockam::identity::{Identifier, Identity}; +use ockam_core::async_trait; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::cli_state::{CredentialsRepository, NamedCredential}; + +#[derive(Clone)] +pub struct CredentialsSqlxDatabase { + database: Arc, +} + +impl CredentialsSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for credentials"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("credentials").await?, + ))) + } +} + +#[async_trait] +impl CredentialsRepository for CredentialsSqlxDatabase { + async fn store_credential( + &self, + name: &str, + issuer: &Identity, + credential: CredentialAndPurposeKey, + ) -> Result { + let query = query("INSERT OR REPLACE INTO credential VALUES (?, ?, ?, ?)") + .bind(name.to_sql()) + .bind(issuer.identifier().to_sql()) + .bind(issuer.change_history().to_sql()) + .bind(CredentialAndPurposeKeySql(credential.clone()).to_sql()); + query.execute(&self.database.pool).await.void()?; + Ok(NamedCredential::new(name, issuer, credential)) + } + + async fn get_credential(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM credential WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_credential()).transpose() + } + + async fn get_credentials(&self) -> Result> { + let query = query_as("SELECT * FROM credential"); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + row.iter().map(|r| r.named_credential()).collect() + } +} + +// Database serialization / deserialization + +pub struct CredentialAndPurposeKeySql(pub CredentialAndPurposeKey); + +impl ToSqlxType for CredentialAndPurposeKeySql { + fn to_sql(&self) -> SqlxType { + self.0.encode_as_string().unwrap().to_sql() + } +} + +/// Low-level representation of a row in the credentials table +#[derive(sqlx::FromRow)] +struct CredentialRow { + name: String, + issuer_identifier: String, + issuer_change_history: String, + credential: String, +} + +impl CredentialRow { + pub(crate) fn named_credential(&self) -> Result { + Ok(NamedCredential::make( + &self.name, + self.issuer_identifier()?, + self.change_history()?, + self.credential()?, + )) + } + + pub(crate) fn issuer_identifier(&self) -> Result { + self.issuer_identifier.clone().try_into() + } + + pub(crate) fn change_history(&self) -> Result { + ChangeHistory::import_from_string(&self.issuer_change_history) + } + + pub(crate) fn credential(&self) -> Result { + CredentialAndPurposeKey::decode_from_string(&self.credential) + } +} + +#[cfg(test)] +mod tests { + use ockam::identity::models::CredentialSchemaIdentifier; + use ockam::identity::utils::AttributesBuilder; + use ockam::identity::{identities, Identities}; + use std::time::Duration; + + use super::*; + + #[tokio::test] + async fn test_credentials_repository() -> Result<()> { + let repository = create_repository().await?; + let identities = identities().await?; + + // a credential can be stored under a name + let issuer_identity = identities.identities_creation().create_identity().await?; + let issuer = identities.get_identity(&issuer_identity).await?; + let credential = create_credential(identities.clone(), &issuer_identity).await?; + let named_credential1 = repository + .store_credential("name", &issuer, credential.clone()) + .await?; + + // That credential can be retrieved by name + let result = repository.get_credential("name").await?; + assert_eq!( + result, + Some(NamedCredential::new("name", &issuer, credential)) + ); + + // All the credentials can be retrieved at once + let credential = create_credential(identities, &issuer_identity).await?; + let named_credential2 = repository + .store_credential("name2", &issuer, credential.clone()) + .await?; + let result = repository.get_credentials().await?; + assert_eq!(result, vec![named_credential1, named_credential2]); + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(CredentialsSqlxDatabase::create().await?) + } + + async fn create_credential( + identities: Arc, + issuer: &Identifier, + ) -> Result { + let subject = identities.identities_creation().create_identity().await?; + + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); + + identities + .credentials() + .credentials_creation() + .issue_credential(issuer, &subject, attributes, Duration::from_secs(1)) + .await + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository.rs new file mode 100644 index 00000000000..ac32279cda4 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository.rs @@ -0,0 +1,31 @@ +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::Result; + +use crate::cli_state::enrollments::IdentityEnrollment; + +/// This trait stores the enrollment status for local identities +/// If an identity has been enrolled it is possible to retrieve: +/// +/// - its name (if it has one) +/// - if this the default identity +/// - if an identity was enrolled and when the local node was informed +/// +/// +#[async_trait] +pub trait EnrollmentsRepository: Send + Sync + 'static { + /// Set the identifier as enrolled, and set a timestamp to record the information + async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()>; + + /// Get the list of enrolled identities + async fn get_enrolled_identities(&self) -> Result>; + + /// Get the enrollment statuses for all known identities + async fn get_all_identities_enrollments(&self) -> Result>; + + /// Return true if the default identity is enrolled + async fn is_default_identity_enrolled(&self) -> Result; + + /// Return true if the identity with the given name is enrolled + async fn is_identity_enrolled(&self, name: &str) -> Result; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository_sql.rs new file mode 100644 index 00000000000..bb2fef2123e --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository_sql.rs @@ -0,0 +1,244 @@ +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::FromRow; +use sqlx::*; +use time::OffsetDateTime; + +use ockam::identity::Identifier; +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_core::async_trait; +use ockam_core::Result; + +use crate::cli_state::enrollments::IdentityEnrollment; +use crate::cli_state::EnrollmentsRepository; + +pub struct EnrollmentsSqlxDatabase { + database: Arc, +} + +impl EnrollmentsSqlxDatabase { + pub fn new(database: Arc) -> Self { + debug!("create a repository for enrollments"); + Self { database } + } + + /// Create a new in-memory database + #[allow(unused)] + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("enrollments").await?, + ))) + } +} + +#[async_trait] +impl EnrollmentsRepository for EnrollmentsSqlxDatabase { + async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()> { + let query = query("INSERT OR REPLACE INTO identity_enrollment VALUES (?, ?)") + .bind(identifier.to_sql()) + .bind(OffsetDateTime::now_utc().to_sql()); + Ok(query.execute(&self.database.pool).await.void()?) + } + + async fn get_enrolled_identities(&self) -> Result> { + let query = query_as( + r#" + SELECT + identity.identifier, named_identity.name, named_identity.is_default, + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + "#, + ) + .bind(None as Option); + let result: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + result + .into_iter() + .map(|r| r.identity_enrollment()) + .collect::>>() + } + + async fn get_all_identities_enrollments(&self) -> Result> { + let query = query_as( + r#" + SELECT + identity.identifier, named_identity.name, named_identity.is_default, + identity_enrollment.enrolled_at + FROM identity + LEFT JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + "#, + ); + let result: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + result + .into_iter() + .map(|r| r.identity_enrollment()) + .collect::>>() + } + + async fn is_default_identity_enrolled(&self) -> Result { + let query = query( + r#" + SELECT + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + WHERE + named_identity.is_default = ? + "#, + ) + .bind(true.to_sql()); + let result: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(result.map(|_| true).unwrap_or(false)) + } + + async fn is_identity_enrolled(&self, name: &str) -> Result { + let query = query( + r#" + SELECT + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + WHERE + named_identity.name = ? + "#, + ) + .bind(name.to_sql()); + let result: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(result.map(|_| true).unwrap_or(false)) + } +} + +#[derive(FromRow)] +pub struct EnrollmentRow { + identifier: String, + name: Option, + is_default: bool, + enrolled_at: Option, +} + +impl EnrollmentRow { + fn identity_enrollment(&self) -> Result { + let identifier = Identifier::from_str(self.identifier.as_str())?; + Ok(IdentityEnrollment::new( + identifier, + self.name.clone(), + self.is_default, + self.enrolled_at(), + )) + } + + fn enrolled_at(&self) -> Option { + self.enrolled_at + .map(|at| OffsetDateTime::from_unix_timestamp(at).unwrap_or(OffsetDateTime::now_utc())) + } +} + +#[cfg(test)] +mod tests { + use crate::cli_state::{EnrollmentsRepository, IdentitiesRepository, IdentitiesSqlxDatabase}; + use ockam::identity::{ + identities, ChangeHistoryRepository, ChangeHistorySqlxDatabase, Identity, + }; + + use super::*; + + #[tokio::test] + async fn test_identities_enrollment_repository() -> Result<()> { + let db = create_database().await?; + let repository = create_repository(db.clone()); + + // create some identities + let identity1 = create_identity(db.clone(), "identity1").await?; + create_identity(db.clone(), "identity2").await?; + + // an identity can be enrolled + repository.set_as_enrolled(identity1.identifier()).await?; + + // retrieve the identities and their enrollment status + let result = repository.get_all_identities_enrollments().await?; + assert_eq!(result.len(), 2); + + // retrieve only the enrolled identities + let result = repository.get_enrolled_identities().await?; + assert_eq!(result.len(), 1); + + // the first identity has been set as the default one when it has been created + // so we should retrieve this information via is_default_identity_enrolled + let result = repository.is_default_identity_enrolled().await?; + assert!(result); + + Ok(()) + } + + /// HELPERS + async fn create_identity(db: Arc, name: &str) -> Result { + let identities = identities().await?; + let identifier = identities.identities_creation().create_identity().await?; + let identity = identities.get_identity(&identifier).await?; + store_identity(db, name, identity).await + } + + async fn store_identity( + db: Arc, + name: &str, + identity: Identity, + ) -> Result { + let change_history_repository = create_change_history_repository(db.clone()).await?; + let identities_repository = create_identities_repository(db).await?; + change_history_repository + .store_change_history(identity.identifier(), identity.change_history().clone()) + .await?; + + identities_repository + .store_named_identity(identity.identifier(), name, "vault") + .await?; + if name == "identity1" { + identities_repository + .set_as_default_by_identifier(identity.identifier()) + .await?; + } + Ok(identity) + } + + fn create_repository(db: Arc) -> Arc { + Arc::new(EnrollmentsSqlxDatabase::new(db)) + } + + async fn create_database() -> Result> { + SqlxDatabase::in_memory("enrollments-test").await + } + + async fn create_change_history_repository( + db: Arc, + ) -> Result> { + Ok(Arc::new(ChangeHistorySqlxDatabase::new(db))) + } + + async fn create_identities_repository( + db: Arc, + ) -> Result> { + Ok(Arc::new(IdentitiesSqlxDatabase::new(db))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/identities_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/identities_repository.rs new file mode 100644 index 00000000000..4613df77700 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/identities_repository.rs @@ -0,0 +1,67 @@ +use crate::NamedIdentity; +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::Result; + +/// The identities repository stores metadata about identities +/// which change history have been stored in the ChangeHistoryRepository. +/// +/// It allows to: +/// +/// - associate a user name to an identity +/// - set one (and one only) identity as the default identity +/// - associate a vault name to an identity so that we know where the identity private keys can be found +/// +/// By default the get/delete functions use the identity name as a parameter. +/// When they use the identity identifier instead, this is indicated in the function name: +/// e.g. get_named_identity_by_identifier() +/// +#[async_trait] +pub trait IdentitiesRepository: Send + Sync + 'static { + /// Associate a name to an identity + async fn store_named_identity( + &self, + identifier: &Identifier, + name: &str, + vault_name: &str, + ) -> Result; + + /// Delete an identity given its name and return its identifier + async fn delete_identity(&self, name: &str) -> Result>; + + /// Delete an identity given its identifier and return its name + async fn delete_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result>; + + /// Return the identifier associated to a named identity + async fn get_identifier(&self, name: &str) -> Result>; + + /// Return the name associated to an identifier + async fn get_identity_name_by_identifier( + &self, + identifier: &Identifier, + ) -> Result>; + + /// Return the named identity with a specific name + async fn get_named_identity(&self, name: &str) -> Result>; + + /// Return the named identity associated to an identifier + async fn get_named_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result>; + + /// Return identities which have been given a name + async fn get_named_identities(&self) -> Result>; + + /// Set an identity as the default one, given its name + async fn set_as_default(&self, name: &str) -> Result<()>; + + /// Set an identity as the default one, given its identifier + async fn set_as_default_by_identifier(&self, identifier: &Identifier) -> Result<()>; + + /// Return the default named identity + async fn get_default_named_identity(&self) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/identities_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/identities_repository_sql.rs new file mode 100644 index 00000000000..540cc010844 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/identities_repository_sql.rs @@ -0,0 +1,308 @@ +use core::str::FromStr; + +use sqlx::*; + +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +use crate::cli_state::{IdentitiesRepository, NamedIdentity}; + +/// Implementation of `IdentitiesRepository` trait based on an underlying database +/// using sqlx as its API, and Sqlite as its driver +#[derive(Clone)] +pub struct IdentitiesSqlxDatabase { + database: Arc, +} + +impl IdentitiesSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for identities"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("identities").await?, + ))) + } +} + +#[async_trait] +impl IdentitiesRepository for IdentitiesSqlxDatabase { + async fn store_named_identity( + &self, + identifier: &Identifier, + name: &str, + vault_name: &str, + ) -> Result { + let transaction = self.database.begin().await.into_core()?; + let is_already_default = self + .get_default_named_identity() + .await? + .map(|n| n.name() == *name) + .unwrap_or(false); + + let query = query("INSERT OR REPLACE INTO named_identity VALUES (?, ?, ?, ?)") + .bind(identifier.to_sql()) + .bind(name.to_sql()) + .bind(vault_name.to_sql()) + .bind(is_already_default.to_sql()); + query.execute(&self.database.pool).await.void()?; + + transaction.commit().await.void()?; + + Ok(NamedIdentity::new( + identifier.clone(), + name.to_string(), + vault_name.to_string(), + is_already_default, + )) + } + + async fn delete_identity(&self, name: &str) -> Result> { + let named_identity = self.get_named_identity(name).await?; + let is_default = named_identity + .clone() + .map(|i| i.is_default()) + .unwrap_or(false); + let query = query("DELETE FROM named_identity WHERE name=?").bind(name.to_sql()); + query.execute(&self.database.pool).await.void()?; + + // if the deleted identity was the default one, select another identity to be the default one + if is_default { + let identities = self.get_named_identities().await?; + if let Some(identity) = identities.first() { + self.set_as_default_by_identifier(&identity.identifier()) + .await?; + }; + } + Ok(named_identity.map(|i| i.identifier())) + } + + async fn delete_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result> { + if let Some(name) = self.get_identity_name_by_identifier(identifier).await? { + self.delete_identity(&name).await?; + Ok(Some(name)) + } else { + Ok(None) + } + } + + async fn get_identifier(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM named_identity WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.identifier()).transpose() + } + + async fn get_identity_name_by_identifier( + &self, + identifier: &Identifier, + ) -> Result> { + let query = + query_as("SELECT * FROM named_identity WHERE identifier=$1").bind(identifier.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.name())) + } + + async fn get_named_identity_by_identifier( + &self, + identifier: &Identifier, + ) -> Result> { + let query = + query_as("SELECT * FROM named_identity WHERE identifier=$1").bind(identifier.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_identity()).transpose() + } + + async fn get_named_identities(&self) -> Result> { + let query = query_as("SELECT * FROM named_identity"); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + row.iter().map(|r| r.named_identity()).collect() + } + + async fn get_named_identity(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM named_identity WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_identity()).transpose() + } + + async fn set_as_default_by_identifier(&self, identifier: &Identifier) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the identifier as the default one + let query1 = query("UPDATE named_identity SET is_default = ? WHERE identifier = ?") + .bind(true.to_sql()) + .bind(identifier.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE named_identity SET is_default = ? WHERE identifier <> ?") + .bind(false.to_sql()) + .bind(identifier.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn set_as_default(&self, name: &str) -> Result<()> { + if let Some(identifier) = self.get_identifier(name).await? { + self.set_as_default_by_identifier(&identifier).await? + }; + Ok(()) + } + + async fn get_default_named_identity(&self) -> Result> { + let query = + query_as("SELECT * FROM named_identity WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_identity()).transpose() + } +} + +#[derive(sqlx::FromRow)] +pub(crate) struct NamedIdentityRow { + identifier: String, + name: String, + vault_name: String, + is_default: bool, +} + +impl NamedIdentityRow { + pub(crate) fn identifier(&self) -> Result { + Identifier::from_str(&self.identifier) + } + + pub(crate) fn name(&self) -> String { + self.name.clone() + } + + #[allow(unused)] + pub(crate) fn vault_name(&self) -> String { + self.vault_name.clone() + } + + pub(crate) fn named_identity(&self) -> Result { + Ok(NamedIdentity::new( + self.identifier()?, + self.name.clone(), + self.vault_name.clone(), + self.is_default, + )) + } +} + +#[cfg(test)] +mod tests { + use ockam::identity::identities; + + use super::*; + + #[tokio::test] + async fn test_identities_repository_named_identities() -> Result<()> { + let repository = create_repository().await?; + + // A name can be associated to an identity + let identifier1 = create_identity().await?; + repository + .store_named_identity(&identifier1, "name1", "vault") + .await?; + + let identifier2 = create_identity().await?; + repository + .store_named_identity(&identifier2, "name2", "vault") + .await?; + + let result = repository.get_identifier("name1").await?; + assert_eq!(result, Some(identifier1.clone())); + + let result = repository + .get_identity_name_by_identifier(&identifier1) + .await?; + assert_eq!(result, Some("name1".into())); + + let result = repository.get_named_identity("name2").await?; + assert_eq!(result.map(|n| n.identifier()), Some(identifier2.clone())); + + let result = repository.get_named_identities().await?; + assert_eq!( + result.iter().map(|n| n.identifier()).collect::>(), + vec![identifier1.clone(), identifier2.clone()] + ); + + repository.delete_identity("name1").await?; + let result = repository.get_named_identities().await?; + assert_eq!( + result.iter().map(|n| n.identifier()).collect::>(), + vec![identifier2.clone()] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_identities_repository_default_identities() -> Result<()> { + let repository = create_repository().await?; + + // A name can be associated to an identity + let identifier1 = create_identity().await?; + let named_identity1 = repository + .store_named_identity(&identifier1, "name1", "vault") + .await?; + + let identifier2 = create_identity().await?; + let named_identity2 = repository + .store_named_identity(&identifier2, "name2", "vault") + .await?; + + // An identity can be marked as being the default one + repository + .set_as_default_by_identifier(&identifier1) + .await?; + let result = repository.get_default_named_identity().await?; + assert_eq!(result, Some(named_identity1.set_as_default())); + + // An identity can be marked as being the default one by passing its name + repository.set_as_default("name2").await?; + let result = repository.get_default_named_identity().await?; + assert_eq!(result, Some(named_identity2.set_as_default())); + + let result = repository.get_named_identity("name1").await?; + assert!(!result.unwrap().is_default()); + + let result = repository.get_default_named_identity().await?; + assert_eq!(result.map(|i| i.name()), Some("name2".to_string())); + + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(IdentitiesSqlxDatabase::create().await?) + } + + async fn create_identity() -> Result { + let identities = identities().await?; + identities.identities_creation().create_identity().await + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/mod.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/mod.rs new file mode 100644 index 00000000000..a294c55ec55 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/mod.rs @@ -0,0 +1,37 @@ +pub use credentials_repository::*; +pub use credentials_repository_sql::*; +pub use enrollments_repository::*; +pub use enrollments_repository_sql::*; +pub use identities_repository::*; +pub use identities_repository_sql::*; +pub use nodes_repository::*; +pub use nodes_repository_sql::*; +pub use projects_repository::*; +pub use projects_repository_sql::*; +pub use spaces_repository::*; +pub use spaces_repository_sql::*; +pub use trust_contexts_repository::*; +pub use trust_contexts_repository_sql::*; +pub use users_repository::*; +pub use users_repository_sql::*; +pub use vaults_repository::*; +pub use vaults_repository_sql::*; + +mod credentials_repository; +mod credentials_repository_sql; +mod enrollments_repository; +mod enrollments_repository_sql; +mod identities_repository; +mod identities_repository_sql; +mod nodes_repository; +mod nodes_repository_sql; +mod projects_repository; +mod projects_repository_sql; +mod spaces_repository; +mod spaces_repository_sql; +mod trust_contexts_repository; +mod trust_contexts_repository_sql; +mod users_repository; +mod users_repository_sql; +mod vaults_repository; +mod vaults_repository_sql; diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/nodes_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/nodes_repository.rs new file mode 100644 index 00000000000..a8e96202159 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/nodes_repository.rs @@ -0,0 +1,64 @@ +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::Result; + +use crate::cli_state::NodeInfo; +use crate::config::lookup::InternetAddress; + +/// This trait supports the storage of node data: +/// +/// - a node has a unique name +/// - a node is always associated to an identifier +/// - a node can be associated to a (single) project +/// - when a node is running we can persist its process id and its TCP listener address +/// - one of the nodes is always set as the default node +/// - a node can be set as an authority node. The purpose of this flag is to be able to display +/// the node status without being able to start a TCP connection since the TCP listener might not be accessible +/// +#[async_trait] +pub trait NodesRepository: Send + Sync + 'static { + /// Store or update the information about a node + async fn store_node(&self, node_info: &NodeInfo) -> Result<()>; + + /// Get the list of all the nodes + async fn get_nodes(&self) -> Result>; + + /// Get a node by name + async fn get_node(&self, node_name: &str) -> Result>; + + /// Get the nodes using a given identifier + async fn get_nodes_by_identifier(&self, identifier: &Identifier) -> Result>; + + /// Get the node set as default + async fn get_default_node(&self) -> Result>; + + /// Set a node set the default node + async fn set_default_node(&self, node_name: &str) -> Result<()>; + + /// Return true if a node with the given name is the default node + async fn is_default_node(&self, node_name: &str) -> Result; + + /// Delete a node given its name + async fn delete_node(&self, node_name: &str) -> Result<()>; + + /// Set the TCP listener of a node + async fn set_tcp_listener_address(&self, node_name: &str, address: &str) -> Result<()>; + + /// Set that node as an authority node + async fn set_as_authority_node(&self, node_name: &str) -> Result<()>; + + /// Get the TCP listener of a node + async fn get_tcp_listener_address(&self, node_name: &str) -> Result>; + + /// Set the process id of a node + async fn set_node_pid(&self, node_name: &str, pid: u32) -> Result<()>; + + /// Unset the process id of a node + async fn set_no_node_pid(&self, node_name: &str) -> Result<()>; + + /// Associate a node to a project + async fn set_node_project_name(&self, node_name: &str, project_name: &str) -> Result<()>; + + /// Return the name of the project associated to a node + async fn get_node_project_name(&self, node_name: &str) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/nodes_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/nodes_repository_sql.rs new file mode 100644 index 00000000000..dcdd1cbd19b --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/nodes_repository_sql.rs @@ -0,0 +1,322 @@ +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::*; + +use ockam::identity::Identifier; +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_core::async_trait; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Result; + +use crate::cli_state::NodesRepository; +use crate::config::lookup::InternetAddress; +use crate::NodeInfo; + +pub struct NodesSqlxDatabase { + database: Arc, +} + +impl NodesSqlxDatabase { + pub fn new(database: Arc) -> Self { + debug!("create a repository for nodes"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new(SqlxDatabase::in_memory("nodes").await?))) + } +} + +#[async_trait] +impl NodesRepository for NodesSqlxDatabase { + async fn store_node(&self, node_info: &NodeInfo) -> Result<()> { + let query = query("INSERT OR REPLACE INTO node VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)") + .bind(node_info.name().to_sql()) + .bind(node_info.identifier().to_sql()) + .bind(node_info.verbosity().to_sql()) + .bind(node_info.is_default().to_sql()) + .bind(node_info.is_authority_node().to_sql()) + .bind( + node_info + .tcp_listener_address() + .as_ref() + .map(|a| a.to_string().to_sql()), + ) + .bind(node_info.pid().map(|p| p.to_sql())); + Ok(query.execute(&self.database.pool).await.void()?) + } + + async fn get_nodes(&self) -> Result> { + let query = query_as("SELECT * FROM node"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + rows.iter().map(|r| r.node_info()).collect() + } + + async fn get_node(&self, node_name: &str) -> Result> { + let query = query_as("SELECT * FROM node WHERE name = ?").bind(node_name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.node_info()).transpose() + } + + async fn get_nodes_by_identifier(&self, identifier: &Identifier) -> Result> { + let query = query_as("SELECT * FROM node WHERE identifier = ?").bind(identifier.to_sql()); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + rows.iter().map(|r| r.node_info()).collect() + } + + async fn get_default_node(&self) -> Result> { + let query = query_as("SELECT * FROM node WHERE is_default = ?").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.node_info()).transpose() + } + + async fn is_default_node(&self, node_name: &str) -> Result { + let query = query("SELECT is_default FROM node WHERE name = ?").bind(node_name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.get(0)).unwrap_or(false)) + } + + async fn set_default_node(&self, node_name: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the node as the default one + let query1 = query("UPDATE node SET is_default = ? WHERE name = ?") + .bind(true.to_sql()) + .bind(node_name.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE node SET is_default = ? WHERE name <> ?") + .bind(false.to_sql()) + .bind(node_name.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn delete_node(&self, node_name: &str) -> Result<()> { + let query = query("DELETE FROM node WHERE name=?").bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn set_tcp_listener_address(&self, node_name: &str, address: &str) -> Result<()> { + let query = query("UPDATE node SET tcp_listener_address = ? WHERE name = ?") + .bind(address.to_sql()) + .bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn set_as_authority_node(&self, node_name: &str) -> Result<()> { + let query = query("UPDATE node SET is_authority = ? WHERE name = ?") + .bind(true.to_sql()) + .bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_tcp_listener_address(&self, node_name: &str) -> Result> { + Ok(self + .get_node(node_name) + .await? + .and_then(|n| n.tcp_listener_address())) + } + + async fn set_node_pid(&self, node_name: &str, pid: u32) -> Result<()> { + let query = query("UPDATE node SET pid = ? WHERE name = ?") + .bind(pid.to_sql()) + .bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn set_no_node_pid(&self, node_name: &str) -> Result<()> { + let query = query("UPDATE node SET pid = NULL WHERE name = ?").bind(node_name.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn set_node_project_name(&self, node_name: &str, project_name: &str) -> Result<()> { + let query = query("INSERT OR REPLACE INTO node_project VALUES (?1, ?2)") + .bind(node_name.to_sql()) + .bind(project_name.to_sql()); + Ok(query.execute(&self.database.pool).await.void()?) + } + + async fn get_node_project_name(&self, node_name: &str) -> Result> { + let query = query("SELECT project_name FROM node_project WHERE node_name = ?") + .bind(node_name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let project_name: Option = row.map(|r| r.get(0)); + Ok(project_name) + } +} + +// Database serialization / deserialization + +#[derive(FromRow)] +pub(crate) struct NodeRow { + name: String, + identifier: String, + verbosity: u8, + is_default: bool, + is_authority: bool, + tcp_listener_address: Option, + pid: Option, +} + +impl NodeRow { + pub(crate) fn node_info(&self) -> Result { + let tcp_listener_address = match self.tcp_listener_address.clone() { + None => None, + Some(a) => Some(InternetAddress::new(a.as_str()).ok_or_else(|| { + ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + format!("cannot deserialize the tcp listener address {}", a), + ) + })?), + }; + + Ok(NodeInfo::new( + self.name.clone(), + Identifier::from_str(&self.identifier.clone())?, + self.verbosity, + self.is_default, + self.is_authority, + tcp_listener_address, + self.pid, + )) + } +} + +#[cfg(test)] +mod test { + use ockam::identity::identities; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let repository = create_repository().await?; + let identifier = create_identity().await?; + + // The information about a node can be stored + let node_info1 = NodeInfo::new( + "node1".to_string(), + identifier.clone(), + 0, + false, + false, + InternetAddress::new("127.0.0.1:51591"), + Some(1234), + ); + + repository.store_node(&node_info1).await?; + + // get the node by name + let result = repository.get_node("node1").await?; + assert_eq!(result, Some(node_info1.clone())); + + // get the node by identifier + let result = repository.get_nodes_by_identifier(&identifier).await?; + assert_eq!(result, vec![node_info1.clone()]); + + // the list of all the nodes can be retrieved + let node_info2 = NodeInfo::new( + "node2".to_string(), + identifier.clone(), + 0, + false, + false, + None, + Some(5678), + ); + + repository.store_node(&node_info2).await?; + let result = repository.get_nodes().await?; + assert_eq!(result, vec![node_info1.clone(), node_info2.clone()]); + + // a node can be set as the default + repository.set_default_node("node2").await?; + let result = repository.get_default_node().await?; + assert_eq!(result, Some(node_info2.set_as_default())); + + // a node can be deleted + repository.delete_node("node2").await?; + let result = repository.get_nodes().await?; + assert_eq!(result, vec![node_info1.clone()]); + + // in that case there is no more default node + let result = repository.get_default_node().await?; + assert!(result.is_none()); + Ok(()) + } + + #[tokio::test] + async fn test_an_identity_used_by_two_nodes() -> Result<()> { + let repository = create_repository().await?; + let identifier1 = create_identity().await?; + let identifier2 = create_identity().await?; + + // Create 3 nodes: 2 with the same identifier, 1 with a different identifier + let node_info1 = create_node("node1", &identifier1); + repository.store_node(&node_info1).await?; + + let node_info2 = create_node("node2", &identifier1); + repository.store_node(&node_info2).await?; + + let node_info3 = create_node("node3", &identifier2); + repository.store_node(&node_info3).await?; + + // get the nodes for identifier1 + let result = repository.get_nodes_by_identifier(&identifier1).await?; + assert_eq!(result, vec![node_info1.clone(), node_info2.clone()]); + Ok(()) + } + + #[tokio::test] + async fn test_node_project() -> Result<()> { + let repository = create_repository().await?; + + // a node can be associated to a project name + repository + .set_node_project_name("node_name", "project1") + .await?; + let result = repository.get_node_project_name("node_name").await?; + assert_eq!(result, Some("project1".into())); + + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(NodesSqlxDatabase::create().await?) + } + + async fn create_identity() -> Result { + let identities = identities().await?; + identities.identities_creation().create_identity().await + } + + fn create_node(node_name: &str, identifier: &Identifier) -> NodeInfo { + NodeInfo::new( + node_name.to_string(), + identifier.clone(), + 0, + false, + false, + InternetAddress::new("127.0.0.1:51591"), + Some(1234), + ) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/projects_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/projects_repository.rs new file mode 100644 index 00000000000..62db3a85379 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/projects_repository.rs @@ -0,0 +1,36 @@ +use ockam_core::async_trait; +use ockam_core::Result; + +use crate::cloud::project::Project; + +/// This trait supports the storage of projects as retrieved from the Controller +/// +/// - in addition to the project data, we can set a project as the default project +/// - a project is identified by its id by default when getting it or setting it as the default +/// +#[async_trait] +pub trait ProjectsRepository: Send + Sync + 'static { + /// Store a project in the database + /// If the project has already been stored and is updated then we take care of + /// keeping it as the default project if it was before + async fn store_project(&self, project: &Project) -> Result<()>; + + /// Return a project given its id + async fn get_project(&self, project_id: &str) -> Result>; + + /// Return a project given its name + async fn get_project_by_name(&self, name: &str) -> Result>; + + /// Return all the projects + async fn get_projects(&self) -> Result>; + + /// Return the default project + async fn get_default_project(&self) -> Result>; + + /// Set one project as the default project + async fn set_default_project(&self, project_id: &str) -> Result<()>; + + /// Delete a project + /// Return true if the project could be deleted + async fn delete_project(&self, project_id: &str) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/projects_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/projects_repository_sql.rs new file mode 100644 index 00000000000..02cee8ff19a --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/projects_repository_sql.rs @@ -0,0 +1,520 @@ +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::*; + +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::env::FromString; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +use crate::cloud::addon::ConfluentConfig; +use crate::cloud::project::{OktaConfig, Project, ProjectUserRole}; +use crate::cloud::share::{RoleInShare, ShareScope}; +use crate::minicbor_url::Url; + +use super::ProjectsRepository; + +/// The ProjectsSqlxDatabase stores project information in several tables: +/// +/// - project +/// - user_project +/// - user_role +/// - okta_config +/// - confluent_config +/// +#[derive(Clone)] +pub struct ProjectsSqlxDatabase { + database: Arc, +} + +impl ProjectsSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for projects"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("projects").await?, + ))) + } +} + +#[async_trait] +impl ProjectsRepository for ProjectsSqlxDatabase { + async fn store_project(&self, project: &Project) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + let is_already_default = self + .get_default_project() + .await? + .map(|p| p.id == project.id) + .unwrap_or(false); + + // store the project data + let query1 = query( + "INSERT OR REPLACE INTO project VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", + ) + .bind(project.id.to_sql()) + .bind(project.name.to_sql()) + .bind(is_already_default.to_sql()) + .bind(project.space_id.to_sql()) + .bind(project.space_name.to_sql()) + .bind(project.identity.as_ref().map(|r| r.to_sql())) + .bind(project.access_route.to_sql()) + .bind(project.authority_identity.as_ref().map(|r| r.to_sql())) + .bind(project.authority_access_route.as_ref().map(|r| r.to_sql())) + .bind(project.version.as_ref().map(|r| r.to_sql())) + .bind(project.running.as_ref().map(|r| r.to_sql())) + .bind(project.operation_id.as_ref().map(|r| r.to_sql())); + query1.execute(&self.database.pool).await.void()?; + + // remove any existing users related to that project if any + let query2 = + query("DELETE FROM user_project WHERE project_id=$1").bind(project.id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + // store the users associated to that project + for user_email in &project.users { + let query3 = query("INSERT OR REPLACE INTO user_project VALUES (?, ?)") + .bind(user_email.to_sql()) + .bind(project.id.to_sql()); + query3.execute(&self.database.pool).await.void()?; + } + + // remove any existing user roles related to that project if any + let query2 = query("DELETE FROM user_role WHERE project_id=$1").bind(project.id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + // store the user roles associated to that project + for user_role in &project.user_roles { + let query4 = query("INSERT OR REPLACE INTO user_role VALUES (?, ?, ?, ?, ?)") + .bind(user_role.id.to_sql()) + .bind(project.id.to_sql()) + .bind(user_role.email.to_sql()) + .bind(user_role.role.to_string().to_sql()) + .bind(user_role.scope.to_string().to_sql()); + query4.execute(&self.database.pool).await.void()?; + } + + // store the okta configuration if any + for okta_config in &project.okta_config { + let query5 = query("INSERT OR REPLACE INTO okta_config VALUES (?, ?, ?, ?, ?)") + .bind(project.id.to_sql()) + .bind(okta_config.tenant_base_url.to_string().to_sql()) + .bind(okta_config.client_id.to_sql()) + .bind(okta_config.certificate.to_string().to_sql()) + .bind(okta_config.attributes.join(",").to_string().to_sql()); + query5.execute(&self.database.pool).await.void()?; + } + + // store the confluent configuration if any + for confluent_config in &project.confluent_config { + let query6 = query("INSERT OR REPLACE INTO confluent_config VALUES (?, ?)") + .bind(project.id.to_sql()) + .bind(confluent_config.bootstrap_server.to_sql()); + query6.execute(&self.database.pool).await.void()?; + } + + transaction.commit().await.void() + } + + async fn get_project(&self, project_id: &str) -> Result> { + let query = + query("SELECT project_name FROM project WHERE project_id=$1").bind(project_id.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + match row { + Some(r) => { + let project_name: String = r.get(0); + self.get_project_by_name(&project_name).await + } + None => Ok(None), + } + } + + async fn get_project_by_name(&self, name: &str) -> Result> { + let transaction = self.database.begin().await.into_core()?; + + let query = query_as("SELECT * FROM project WHERE project_name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let project = match row.map(|r| r.project()).transpose()? { + Some(mut project) => { + // get the project users emails + let query2 = query_as("SELECT * FROM user_project WHERE project_id=$1") + .bind(project.id.to_sql()); + let rows: Vec = + query2.fetch_all(&self.database.pool).await.into_core()?; + let users = rows.into_iter().map(|r| r.user_email).collect(); + project.users = users; + + // get the project users roles + let query3 = query_as("SELECT * FROM user_role WHERE project_id=$1") + .bind(project.id.to_sql()); + let rows: Vec = + query3.fetch_all(&self.database.pool).await.into_core()?; + let user_roles: Vec = rows + .into_iter() + .map(|r| r.project_user_role()) + .collect::>>()?; + project.user_roles = user_roles; + + // get the project okta configuration + let query4 = query_as("SELECT * FROM okta_config WHERE project_id=$1") + .bind(project.id.to_sql()); + let row: Option = query4 + .fetch_optional(&self.database.pool) + .await + .into_core()?; + project.okta_config = row.map(|r| r.okta_config()).transpose()?; + + // get the project confluent configuration + let query5 = query_as("SELECT * FROM confluent_config WHERE project_id=$1") + .bind(project.id.to_sql()); + let row: Option = query5 + .fetch_optional(&self.database.pool) + .await + .into_core()?; + project.confluent_config = row.map(|r| r.confluent_config()); + + Some(project) + } + + None => None, + }; + transaction.commit().await.void()?; + Ok(project) + } + + async fn get_projects(&self) -> Result> { + let query = query("SELECT project_name FROM project"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + let project_names: Vec = rows.iter().map(|r| r.get(0)).collect(); + let mut projects = vec![]; + for project_name in project_names { + let project = self.get_project_by_name(&project_name).await?; + if let Some(project) = project { + projects.push(project); + }; + } + Ok(projects) + } + + async fn get_default_project(&self) -> Result> { + let query = + query("SELECT project_name FROM project WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + match row { + Some(r) => { + let project_name: String = r.get(0); + self.get_project_by_name(&project_name).await + } + None => Ok(None), + } + } + + async fn set_default_project(&self, project_id: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the project as the default one + let query1 = query("UPDATE project SET is_default = ? WHERE project_id = ?") + .bind(true.to_sql()) + .bind(project_id.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE project SET is_default = ? WHERE project_id <> ?") + .bind(false.to_sql()) + .bind(project_id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn delete_project(&self, project_id: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + + let query1 = query("DELETE FROM project WHERE project_id=?").bind(project_id.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + let query2 = query("DELETE FROM user_project WHERE project_id=?").bind(project_id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + let query3 = query("DELETE FROM user_role WHERE project_id=?").bind(project_id.to_sql()); + query3.execute(&self.database.pool).await.void()?; + + let query4 = query("DELETE FROM okta_config WHERE project_id=?").bind(project_id.to_sql()); + query4.execute(&self.database.pool).await.void()?; + + let query5 = + query("DELETE FROM confluent_config WHERE project_id=?").bind(project_id.to_sql()); + query5.execute(&self.database.pool).await.void()?; + + transaction.commit().await.void()?; + Ok(()) + } +} + +// Database serialization / deserialization + +/// Low-level representation of a row in the projects table +#[derive(sqlx::FromRow)] +struct ProjectRow { + project_id: String, + project_name: String, + #[allow(unused)] + is_default: bool, + space_id: String, + space_name: String, + identifier: Option, + access_route: String, + authority_identity: Option, + authority_access_route: Option, + version: Option, + running: Option, + operation_id: Option, +} + +impl ProjectRow { + pub(crate) fn project(&self) -> Result { + self.complete_project(vec![], vec![], None, None) + } + + pub(crate) fn complete_project( + &self, + user_emails: Vec, + user_roles: Vec, + okta_config: Option, + confluent_config: Option, + ) -> Result { + let identifier = self + .identifier + .as_ref() + .map(|i| Identifier::from_string(i)) + .transpose()?; + Ok(Project { + id: self.project_id.clone(), + name: self.project_name.clone(), + space_id: self.space_id.clone(), + space_name: self.space_name.clone(), + identity: identifier, + access_route: self.access_route.clone(), + authority_access_route: self.authority_access_route.clone(), + authority_identity: self.authority_identity.clone(), + version: self.version.clone(), + running: self.running, + operation_id: self.operation_id.clone(), + users: user_emails, + user_roles, + okta_config, + confluent_config, + }) + } +} + +/// Low-level representation of a row in the user_project table +#[derive(sqlx::FromRow)] +struct UserProjectRow { + #[allow(unused)] + project_id: String, + user_email: String, +} + +/// Low-level representation of a row in the user_role table +#[derive(sqlx::FromRow)] +struct UserRoleRow { + user_id: i64, + #[allow(unused)] + project_id: String, + user_email: String, + role: String, + scope: String, +} + +impl UserRoleRow { + fn project_user_role(&self) -> Result { + let role = RoleInShare::from_str(&self.role) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + let scope = ShareScope::from_str(&self.scope) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + Ok(ProjectUserRole { + id: self.user_id as u64, + email: self.user_email.clone(), + role, + scope, + }) + } +} + +/// Low-level representation of a row in the okta_config table +#[derive(sqlx::FromRow)] +struct OktaConfigRow { + #[allow(unused)] + project_id: String, + tenant_base_url: String, + client_id: String, + certificate: String, + attributes: String, +} + +impl OktaConfigRow { + fn okta_config(&self) -> Result { + let tenant_base_url = Url::parse(&self.tenant_base_url.clone()) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + Ok(OktaConfig { + tenant_base_url, + certificate: self.certificate.clone(), + client_id: self.client_id.clone(), + attributes: self.attributes.split(',').map(|a| a.to_string()).collect(), + }) + } +} + +/// Low-level representation of a row in the confluent_config table +#[derive(sqlx::FromRow)] +struct ConfluentConfigRow { + #[allow(unused)] + project_id: String, + bootstrap_server: String, +} + +impl ConfluentConfigRow { + fn confluent_config(&self) -> ConfluentConfig { + ConfluentConfig { + bootstrap_server: self.bootstrap_server.clone(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let repository = create_repository().await?; + + // create and store 2 projects + let project1 = create_project( + "1", + "name1", + vec!["me@ockam.io", "you@ockam.io"], + vec![ + create_project_user_role(1, RoleInShare::Admin), + create_project_user_role(2, RoleInShare::Guest), + ], + ); + let mut project2 = create_project( + "2", + "name2", + vec!["me@ockam.io", "him@ockam.io", "her@ockam.io"], + vec![ + create_project_user_role(1, RoleInShare::Admin), + create_project_user_role(2, RoleInShare::Guest), + ], + ); + repository.store_project(&project1).await?; + repository.store_project(&project2).await?; + + // retrieve them as a list or by name + let result = repository.get_projects().await?; + assert_eq!(result, vec![project1.clone(), project2.clone()]); + + let result = repository.get_project_by_name("name1").await?; + assert_eq!(result, Some(project1.clone())); + + // a project can be marked as the default project + repository.set_default_project("1").await?; + let result = repository.get_default_project().await?; + assert_eq!(result, Some(project1.clone())); + + repository.set_default_project("2").await?; + let result = repository.get_default_project().await?; + assert_eq!(result, Some(project2.clone())); + + // updating a project which was already the default should keep it the default + project2.users = vec!["someone@ockam.io".into()]; + repository.store_project(&project2).await?; + let result = repository.get_default_project().await?; + assert_eq!(result, Some(project2.clone())); + + // a project can be deleted + repository.delete_project("2").await?; + let result = repository.get_default_project().await?; + assert_eq!(result, None); + + let result = repository.get_projects().await?; + assert_eq!(result, vec![project1.clone()]); + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(ProjectsSqlxDatabase::create().await?) + } + + fn create_project( + id: &str, + name: &str, + user_emails: Vec<&str>, + user_roles: Vec, + ) -> Project { + Project { + id: id.into(), + name: name.into(), + space_id: "space-id".into(), + space_name: "space-name".into(), + access_route: "route".into(), + users: user_emails.iter().map(|u| u.to_string()).collect(), + identity: Some( + Identifier::from_str( + "I124ed0b2e5a2be82e267ead6b3279f683616b66da1b2c3d4e5f6a6b5c4d3e2f1", + ) + .unwrap(), + ), + authority_access_route: Some("authority-route".into()), + authority_identity: Some("authority-identity".into()), + okta_config: Some(create_okta_config()), + confluent_config: Some(create_confluent_config()), + version: Some("1.0".into()), + running: Some(true), + operation_id: Some("abc".into()), + user_roles, + } + } + + fn create_project_user_role(user_id: u64, role: RoleInShare) -> ProjectUserRole { + ProjectUserRole { + email: "user_email".into(), + id: user_id, + role, + scope: ShareScope::Project, + } + } + + fn create_okta_config() -> OktaConfig { + OktaConfig { + tenant_base_url: Url::parse("http://ockam.io").unwrap(), + certificate: "certificate".to_string(), + client_id: "client-id".to_string(), + attributes: vec!["attribute1".into(), "attribute2".into()], + } + } + + fn create_confluent_config() -> ConfluentConfig { + ConfluentConfig { + bootstrap_server: "bootstrap_server".to_string(), + } + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository.rs new file mode 100644 index 00000000000..a5d7fcc0f29 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository.rs @@ -0,0 +1,32 @@ +use crate::cloud::space::Space; +use ockam_core::async_trait; +use ockam_core::Result; + +/// This trait supports the storage of spaces as retrieved from the Controller +/// +/// - in addition to the space data, we can set a space as the default space +/// - a space is identified by its id by default when getting it or setting it as the default +/// +#[async_trait] +pub trait SpacesRepository: Send + Sync + 'static { + /// Store a space + async fn store_space(&self, space: &Space) -> Result<()>; + + /// Return a space for a given id + async fn get_space(&self, space_id: &str) -> Result>; + + /// Return a space for a given name + async fn get_space_by_name(&self, name: &str) -> Result>; + + /// Return the list of all spaces + async fn get_spaces(&self) -> Result>; + + /// Return the default space + async fn get_default_space(&self) -> Result>; + + /// Set a space as the default one + async fn set_default_space(&self, space_id: &str) -> Result<()>; + + /// Delete a space + async fn delete_space(&self, space_id: &str) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository_sql.rs new file mode 100644 index 00000000000..273e95f5f09 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/spaces_repository_sql.rs @@ -0,0 +1,261 @@ +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::*; + +use ockam_core::async_trait; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +use crate::cloud::space::Space; + +use super::SpacesRepository; + +#[derive(Clone)] +pub struct SpacesSqlxDatabase { + database: Arc, +} + +impl SpacesSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for spaces"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("spaces").await?, + ))) + } +} + +#[async_trait] +impl SpacesRepository for SpacesSqlxDatabase { + async fn store_space(&self, space: &Space) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + let is_already_default = self + .get_default_space() + .await? + .map(|s| s.id == space.id) + .unwrap_or(false); + + let query1 = query("INSERT OR REPLACE INTO space VALUES (?, ?, ?)") + .bind(space.id.to_sql()) + .bind(space.name.to_sql()) + .bind(is_already_default.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // remove any existing users related to that space if any + let query2 = query("DELETE FROM user_space WHERE space_id=$1").bind(space.id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + // store the users associated to that space + for user_email in &space.users { + let query3 = query("INSERT OR REPLACE INTO user_space VALUES (?, ?)") + .bind(user_email.to_sql()) + .bind(space.id.to_sql()); + query3.execute(&self.database.pool).await.void()?; + } + + transaction.commit().await.void() + } + + async fn get_space(&self, space_id: &str) -> Result> { + let query = query("SELECT space_name FROM space WHERE space_id=$1").bind(space_id.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + match row { + Some(r) => { + let space_name: String = r.get(0); + self.get_space_by_name(&space_name).await + } + None => Ok(None), + } + } + + async fn get_space_by_name(&self, name: &str) -> Result> { + let transaction = self.database.begin().await.into_core()?; + + let query1 = query_as("SELECT * FROM space WHERE space_name=$1").bind(name.to_sql()); + let row: Option = query1 + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let space = match row.map(|r| r.space()) { + Some(mut space) => { + let query2 = + query_as("SELECT * FROM user_space WHERE space_id=$1").bind(space.id.to_sql()); + let rows: Vec = + query2.fetch_all(&self.database.pool).await.into_core()?; + let users = rows.into_iter().map(|r| r.user_email).collect(); + space.users = users; + Some(space) + } + None => None, + }; + transaction.commit().await.void()?; + Ok(space) + } + + async fn get_spaces(&self) -> Result> { + let transaction = self.database.begin().await.into_core()?; + + let query = query_as("SELECT * FROM space"); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + + let mut spaces = vec![]; + for space_row in row { + let query2 = query_as("SELECT * FROM user_space WHERE space_id=$1") + .bind(space_row.space_id.to_sql()); + let rows: Vec = + query2.fetch_all(&self.database.pool).await.into_core()?; + let users = rows.into_iter().map(|r| r.user_email).collect(); + spaces.push(space_row.space_with_user_emails(users)) + } + + transaction.commit().await.void()?; + + Ok(spaces) + } + + async fn get_default_space(&self) -> Result> { + let query = query("SELECT space_name FROM space WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let name: Option = row.map(|r| r.get(0)); + match name { + Some(name) => self.get_space_by_name(&name).await, + None => Ok(None), + } + } + + async fn set_default_space(&self, space_id: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the space as the default one + let query1 = query("UPDATE space SET is_default = ? WHERE space_id = ?") + .bind(true.to_sql()) + .bind(space_id.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE space SET is_default = ? WHERE space_id <> ?") + .bind(false.to_sql()) + .bind(space_id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn delete_space(&self, space_id: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + + let query1 = query("DELETE FROM space WHERE space_id=?").bind(space_id.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + let query2 = query("DELETE FROM user_space WHERE space_id=?").bind(space_id.to_sql()); + query2.execute(&self.database.pool).await.void()?; + + transaction.commit().await.void() + } +} + +// Database serialization / deserialization + +/// Low-level representation of a row in the space table +#[derive(sqlx::FromRow)] +struct SpaceRow { + space_id: String, + space_name: String, +} + +impl SpaceRow { + pub(crate) fn space(&self) -> Space { + self.space_with_user_emails(vec![]) + } + + pub(crate) fn space_with_user_emails(&self, user_emails: Vec) -> Space { + Space { + id: self.space_id.clone(), + name: self.space_name.clone(), + users: user_emails, + } + } +} + +/// Low-level representation of a row in the user_space table +#[derive(sqlx::FromRow)] +struct UserSpaceRow { + #[allow(unused)] + space_id: String, + user_email: String, +} + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let repository = create_repository().await?; + + // create and store 2 spaces + let space1 = Space { + id: "1".to_string(), + name: "name1".to_string(), + users: vec!["me@ockam.io".to_string(), "you@ockam.io".to_string()], + }; + let mut space2 = Space { + id: "2".to_string(), + name: "name2".to_string(), + users: vec![ + "me@ockam.io".to_string(), + "him@ockam.io".to_string(), + "her@ockam.io".to_string(), + ], + }; + + repository.store_space(&space1).await?; + repository.store_space(&space2).await?; + + // retrieve them as a vector or by name + let result = repository.get_spaces().await?; + assert_eq!(result, vec![space1.clone(), space2.clone()]); + + let result = repository.get_space_by_name("name1").await?; + assert_eq!(result, Some(space1.clone())); + + // a space can be marked as the default space + repository.set_default_space("1").await?; + let result = repository.get_default_space().await?; + assert_eq!(result, Some(space1.clone())); + + repository.set_default_space("2").await?; + let result = repository.get_default_space().await?; + assert_eq!(result, Some(space2.clone())); + + // updating a space which was already the default should keep it the default + space2.users = vec!["someone@ockam.io".to_string()]; + repository.store_space(&space2).await?; + let result = repository.get_default_space().await?; + assert_eq!(result, Some(space2.clone())); + + // a space can be deleted + repository.delete_space("2").await?; + let result = repository.get_default_space().await?; + assert_eq!(result, None); + + let result = repository.get_spaces().await?; + assert_eq!(result, vec![space1.clone()]); + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(SpacesSqlxDatabase::create().await?) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/trust_contexts_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/trust_contexts_repository.rs new file mode 100644 index 00000000000..c180ce429a7 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/trust_contexts_repository.rs @@ -0,0 +1,28 @@ +use crate::cli_state::NamedTrustContext; +use ockam_core::async_trait; +use ockam_core::Result; + +/// This trait supports the storage of trust context data: +/// +/// - one single trust context can be set as the default one +/// +#[async_trait] +pub trait TrustContextsRepository: Send + Sync + 'static { + /// Store trust context data associated with a specific trust context name + async fn store_trust_context(&self, trust_context: &NamedTrustContext) -> Result<()>; + + /// Get the default named trust context + async fn get_default_trust_context(&self) -> Result>; + + /// Set a trust context as the default one + async fn set_default_trust_context(&self, name: &str) -> Result<()>; + + /// Get a named trust context by name + async fn get_trust_context(&self, name: &str) -> Result>; + + /// Get all named trust contexts + async fn get_trust_contexts(&self) -> Result>; + + /// Delete a trust context + async fn delete_trust_context(&self, name: &str) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/trust_contexts_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/trust_contexts_repository_sql.rs new file mode 100644 index 00000000000..440b23a7b03 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/trust_contexts_repository_sql.rs @@ -0,0 +1,261 @@ +use std::sync::Arc; + +use sqlx::*; + +use ockam::identity::models::{ChangeHistory, CredentialAndPurposeKey}; +use ockam::identity::SecureChannels; +use ockam::identity::TrustContext; +use ockam_core::async_trait; +use ockam_core::env::FromString; +use ockam_core::Result; +use ockam_multiaddr::MultiAddr; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_transport_tcp::TcpTransport; + +use crate::cli_state::storage::CredentialAndPurposeKeySql; +use crate::cli_state::storage::TrustContextsRepository; +use crate::NamedTrustContext; + +#[derive(Clone)] +pub struct TrustContextsSqlxDatabase { + database: Arc, +} + +impl TrustContextsSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for trust contexts"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("trust contexts").await?, + ))) + } +} + +#[async_trait] +impl TrustContextsRepository for TrustContextsSqlxDatabase { + async fn store_trust_context(&self, trust_context: &NamedTrustContext) -> Result<()> { + let is_already_default = self + .get_default_trust_context() + .await? + .map(|tc| tc.name() == trust_context.name()) + .unwrap_or(false); + + let query = query("INSERT OR REPLACE INTO trust_context VALUES ($1, $2, $3, $4, $5, $6)") + .bind(trust_context.name().to_sql()) + .bind(trust_context.trust_context_id().to_sql()) + .bind(is_already_default.to_sql()) + .bind( + trust_context + .credential() + .as_ref() + .map(|c| CredentialAndPurposeKeySql(c.clone()).to_sql()), + ) + .bind( + trust_context + .authority_change_history() + .as_ref() + .map(|c| c.to_sql()), + ) + .bind( + trust_context + .authority_route() + .as_ref() + .map(|r| r.to_string().to_sql()), + ); + query.execute(&self.database.pool).await.void() + } + + async fn get_default_trust_context(&self) -> Result> { + let query = query_as("SELECT * FROM trust_context WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_trust_context()).transpose() + } + + async fn set_default_trust_context(&self, name: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the trust context as the default one + let query1 = query("UPDATE trust_context SET is_default = ? WHERE name = ?") + .bind(true.to_sql()) + .bind(name.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE trust_context SET is_default = ? WHERE name <> ?") + .bind(false.to_sql()) + .bind(name.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn get_trust_context(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM trust_context WHERE name=$1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|u| u.named_trust_context()).transpose() + } + + async fn get_trust_contexts(&self) -> Result> { + let query = query_as("SELECT * FROM trust_context"); + let rows: Vec = + query.fetch_all(&self.database.pool).await.into_core()?; + rows.iter().map(|u| u.named_trust_context()).collect() + } + + async fn delete_trust_context(&self, name: &str) -> Result<()> { + let query1 = query("DELETE FROM trust_context WHERE name=?").bind(name.to_sql()); + query1.execute(&self.database.pool).await.void() + } +} + +// Database serialization / deserialization + +#[derive(sqlx::FromRow)] +struct NamedTrustContextRow { + name: String, + trust_context_id: String, + #[allow(unused)] + is_default: bool, + credential: Option, + authority_change_history: Option, + authority_route: Option, +} + +impl NamedTrustContextRow { + fn named_trust_context(&self) -> Result { + let credential: Option = self + .credential + .as_ref() + .map(|c| CredentialAndPurposeKey::decode_from_string(c)) + .transpose()?; + let authority_change_history = self + .authority_change_history + .as_ref() + .map(|i| ChangeHistory::import_from_string(i)) + .transpose()?; + let authority_route = self + .authority_route + .as_ref() + .map(|r| MultiAddr::from_string(r)) + .transpose()?; + Ok(NamedTrustContext::new( + &self.name, + &self.trust_context_id, + credential, + authority_change_history, + authority_route, + )) + } + + #[allow(unused)] + async fn trust_context( + &self, + tcp_transport: &TcpTransport, + secure_channels: Arc, + ) -> Result { + Ok(self + .named_trust_context()? + .trust_context(tcp_transport, secure_channels) + .await?) + } +} + +#[cfg(test)] +mod test { + use core::time::Duration; + + use ockam::identity::models::CredentialSchemaIdentifier; + use ockam::identity::utils::AttributesBuilder; + use ockam::identity::{identities, Identifier, Identities, Identity}; + + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let repository = create_repository().await?; + + // create 2 trust contexts + let identities = identities().await?; + let issuer_identifier = identities.identities_creation().create_identity().await?; + let issuer = identities.get_identity(&issuer_identifier).await?; + + let trust_context1 = + create_trust_context("trust-context-1", identities.clone(), &issuer).await?; + let trust_context2 = create_trust_context("trust-context-2", identities, &issuer).await?; + repository.store_trust_context(&trust_context1).await?; + repository.store_trust_context(&trust_context2).await?; + + // get a trust context by name + let result = repository.get_trust_context("trust-context-1").await?; + assert_eq!(result, Some(trust_context1.clone())); + + // get all the trust contexts + let result = repository.get_trust_contexts().await?; + assert_eq!(result, vec![trust_context1.clone(), trust_context2.clone()]); + + // set the first trust context as the default trust context + repository + .set_default_trust_context("trust-context-1") + .await?; + let result = repository.get_default_trust_context().await?; + assert_eq!(result, Some(trust_context1)); + + // then set the second one + repository + .set_default_trust_context("trust-context-2") + .await?; + let result = repository.get_default_trust_context().await?; + assert_eq!(result, Some(trust_context2.clone())); + + // a trust context can be deleted + repository.delete_trust_context("trust-context-1").await?; + let result = repository.get_trust_contexts().await?; + assert_eq!(result, vec![trust_context2]); + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(TrustContextsSqlxDatabase::create().await?) + } + + async fn create_trust_context( + name: &str, + identities: Arc, + issuer: &Identity, + ) -> Result { + Ok(NamedTrustContext::new( + name, + name, + Some(create_credential(identities, issuer.identifier()).await?), + Some(issuer.change_history().clone()), + Some(MultiAddr::from_string("/dnsaddr/k8s-hubdev-hubconso-85b649e0fe-0c482fd0e8117e9d.elb.us-west-1.amazonaws.com/tcp/6252/service/api").unwrap()), + )) + } + + async fn create_credential( + identities: Arc, + issuer: &Identifier, + ) -> Result { + let subject = identities.identities_creation().create_identity().await?; + + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); + + identities + .credentials() + .credentials_creation() + .issue_credential(issuer, &subject, attributes, Duration::from_secs(1)) + .await + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/users_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/users_repository.rs new file mode 100644 index 00000000000..42f50bc2a90 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/users_repository.rs @@ -0,0 +1,39 @@ +use crate::cloud::enroll::auth0::UserInfo; +use ockam_core::async_trait; +use ockam_core::Result; + +/// This traits allows user information to be stored locally. +/// User information is retrieved when a user has been authenticated. +/// It contains fields like: +/// +/// - name +/// - sub(ject) unique identifier +/// - email +/// - etc... +/// +/// Even if there is a sub field supposed to uniquely identify a user we currently use +/// the user email for this. +/// +/// A user can also be set as the default user via this repository. +/// +#[async_trait] +pub trait UsersRepository: Send + Sync + 'static { + /// Store (or update) some information + /// In case of an update, if the user was already the default user, it will stay the default user + async fn store_user(&self, user: &UserInfo) -> Result<()>; + + /// Return the default user + async fn get_default_user(&self) -> Result>; + + /// Set a user as the default one + async fn set_default_user(&self, email: &str) -> Result<()>; + + /// Return a user given their email + async fn get_user(&self, email: &str) -> Result>; + + /// Get the list of all users + async fn get_users(&self) -> Result>; + + /// Delete a user given their email + async fn delete_user(&self, email: &str) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/users_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/users_repository_sql.rs new file mode 100644 index 00000000000..6c557c777c3 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/users_repository_sql.rs @@ -0,0 +1,181 @@ +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::*; + +use ockam_core::async_trait; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; + +use crate::cloud::enroll::auth0::UserInfo; + +use super::UsersRepository; + +#[derive(Clone)] +pub struct UsersSqlxDatabase { + database: Arc, +} + +impl UsersSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for users"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new(SqlxDatabase::in_memory("users").await?))) + } +} + +#[async_trait] +impl UsersRepository for UsersSqlxDatabase { + async fn store_user(&self, user: &UserInfo) -> Result<()> { + let is_already_default = self + .get_default_user() + .await? + .map(|u| u.email == user.email) + .unwrap_or(false); + + let query = query("INSERT OR REPLACE INTO user VALUES ($1, $2, $3, $4, $5, $6, $7, $8)") + .bind(user.email.to_sql()) + .bind(user.sub.to_sql()) + .bind(user.nickname.to_sql()) + .bind(user.name.to_sql()) + .bind(user.picture.to_sql()) + .bind(user.updated_at.to_sql()) + .bind(user.email_verified.to_sql()) + .bind(is_already_default.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_default_user(&self) -> Result> { + let query = query("SELECT email FROM user WHERE is_default=$1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + let email: Option = row.map(|r| r.get(0)); + match email { + Some(email) => self.get_user(&email).await, + None => Ok(None), + } + } + + async fn set_default_user(&self, email: &str) -> Result<()> { + let query = query("UPDATE user SET is_default = ? WHERE email = ?") + .bind(true.to_sql()) + .bind(email.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_user(&self, email: &str) -> Result> { + let query = query_as("SELECT * FROM user WHERE email=$1").bind(email.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|u| u.user())) + } + + async fn get_users(&self) -> Result> { + let query = query_as("SELECT * FROM user"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + Ok(rows.iter().map(|u| u.user()).collect()) + } + + async fn delete_user(&self, email: &str) -> Result<()> { + let query1 = query("DELETE FROM user WHERE email=?").bind(email.to_sql()); + query1.execute(&self.database.pool).await.void() + } +} + +// Database serialization / deserialization + +/// Low-level representation of a row in the user table +#[derive(sqlx::FromRow)] +struct UserRow { + email: String, + sub: String, + nickname: String, + name: String, + picture: String, + updated_at: String, + email_verified: bool, + #[allow(unused)] + is_default: bool, +} + +impl UserRow { + fn user(&self) -> UserInfo { + UserInfo { + email: self.email.clone(), + sub: self.sub.clone(), + nickname: self.nickname.clone(), + name: self.name.clone(), + picture: self.picture.clone(), + updated_at: self.updated_at.clone(), + email_verified: self.email_verified, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let repository = create_repository().await?; + + // create and store 2 users + let user1 = UserInfo { + sub: "sub".into(), + nickname: "me".to_string(), + name: "me".to_string(), + picture: "me".to_string(), + updated_at: "today".to_string(), + email: "me@ockam.io".into(), + email_verified: false, + }; + let user2 = UserInfo { + sub: "sub".into(), + nickname: "you".to_string(), + name: "you".to_string(), + picture: "you".to_string(), + updated_at: "today".to_string(), + email: "you@ockam.io".into(), + email_verified: false, + }; + + repository.store_user(&user1).await?; + repository.store_user(&user2).await?; + + // retrieve them as a vector or by name + let result = repository.get_users().await?; + assert_eq!(result, vec![user1.clone(), user2.clone()]); + + let result = repository.get_user("me@ockam.io").await?; + assert_eq!(result, Some(user1.clone())); + + // a user can be set created as the default user + repository.set_default_user("me@ockam.io").await?; + let result = repository.get_default_user().await?; + assert_eq!(result, Some(user1.clone())); + + // a user can be deleted + repository.delete_user("you@ockam.io").await?; + let result = repository.get_user("you@ockam.io").await?; + assert_eq!(result, None); + + let result = repository.get_users().await?; + assert_eq!(result, vec![user1.clone()]); + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(UsersSqlxDatabase::create().await?) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository.rs new file mode 100644 index 00000000000..4311c3bac1f --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use ockam_core::async_trait; +use ockam_core::Result; + +use crate::NamedVault; + +/// This trait allows vaults to be defined with a name and a path +/// in order to make it possible to store identity keys in different databases on disk (or in a KMS) +#[async_trait] +pub trait VaultsRepository: Send + Sync + 'static { + /// Store a new vault path with an associated name + async fn store_vault(&self, name: &str, path: PathBuf, is_kms: bool) -> Result; + + /// Delete a vault given its name + async fn delete_vault(&self, name: &str) -> Result<()>; + + /// Return a vault by name + async fn get_named_vault(&self, name: &str) -> Result>; + + /// Return all vaults + async fn get_named_vaults(&self) -> Result>; + + /// Return the default vault + async fn get_default_vault(&self) -> Result>; + + /// Set a vault as the default one. Any previous default vault is unset + async fn set_as_default(&self, name: &str) -> Result<()>; + + async fn is_default(&self, name: &str) -> Result; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs new file mode 100644 index 00000000000..c4de1a6e18d --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs @@ -0,0 +1,199 @@ +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::*; + +use crate::cli_state::{NamedVault, VaultsRepository}; +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_core::async_trait; +use ockam_core::Result; + +pub struct VaultsSqlxDatabase { + database: Arc, +} + +impl VaultsSqlxDatabase { + pub fn new(database: Arc) -> Self { + debug!("create a repository for vaults"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("vaults").await?, + ))) + } +} + +#[async_trait] +impl VaultsRepository for VaultsSqlxDatabase { + async fn store_vault(&self, name: &str, path: PathBuf, is_kms: bool) -> Result { + let transaction = self.database.begin().await.into_core()?; + let is_already_default = self + .get_default_vault() + .await? + .map(|v| v.name() == name) + .unwrap_or(false); + let query = query("INSERT OR REPLACE INTO vault VALUES (?1, ?2, ?3, ?4)") + .bind(name.to_sql()) + .bind(path.to_sql()) + .bind(is_already_default.to_sql()) + .bind(is_kms.to_sql()); + query.execute(&self.database.pool).await.void()?; + transaction.commit().await.void()?; + + Ok(NamedVault::new( + name, + path.clone(), + is_already_default, + is_kms, + )) + } + + /// Delete a vault by name + async fn delete_vault(&self, name: &str) -> Result<()> { + let is_default = self + .get_named_vault(name) + .await? + .map(|v| v.is_default()) + .unwrap_or(false); + let query = query("DELETE FROM vault WHERE name = $1").bind(name.to_sql()); + query.execute(&self.database.pool).await.void()?; + + // if the deleted vault was the default one, select another vault to be the default one + if is_default { + let vaults = self.get_named_vaults().await?; + if let Some(vault) = vaults.first() { + self.set_as_default(&vault.name()).await?; + }; + } + Ok(()) + } + + async fn set_as_default(&self, name: &str) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + // set the identifier as the default one + let query1 = query("UPDATE vault SET is_default = ? WHERE name = ?") + .bind(true.to_sql()) + .bind(name.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + // set all the others as non-default + let query2 = query("UPDATE vault SET is_default = ? WHERE name <> ?") + .bind(false.to_sql()) + .bind(name.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void() + } + + async fn is_default(&self, name: &str) -> Result { + let query = query_as("SELECT * FROM vault WHERE name = $1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.is_default()).unwrap_or(false)) + } + + async fn get_named_vaults(&self) -> Result> { + let query = query_as("SELECT * FROM vault"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + rows.iter().map(|r| r.named_vault()).collect() + } + + async fn get_named_vault(&self, name: &str) -> Result> { + let query = query_as("SELECT * FROM vault WHERE name = $1").bind(name.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_vault()).transpose() + } + + async fn get_default_vault(&self) -> Result> { + let query = query_as("SELECT * FROM vault WHERE is_default = $1").bind(true.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.named_vault()).transpose() + } +} + +// Database serialization / deserialization + +#[derive(FromRow)] +pub(crate) struct VaultRow { + name: String, + path: String, + is_default: bool, + is_kms: bool, +} + +impl VaultRow { + pub(crate) fn named_vault(&self) -> Result { + Ok(NamedVault::new( + &self.name, + PathBuf::from_str(self.path.as_str()).unwrap(), + self.is_default, + self.is_kms, + )) + } + + pub(crate) fn is_default(&self) -> bool { + self.is_default + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn test_repository() -> Result<()> { + let repository = create_repository().await?; + + // A vault can be defined with a path and stored under a specific name + let named_vault1 = repository + .store_vault("vault1", "path".into(), false) + .await?; + let expected = NamedVault::new("vault1", "path".into(), false, false); + assert_eq!(named_vault1, expected); + + // The vault can then be retrieved with its name + let result = repository.get_named_vault("vault1").await?; + assert_eq!(result, Some(named_vault1.clone())); + + // A default vault can be set + repository.set_as_default("vault1").await?; + let result = repository.get_default_vault().await?; + assert_eq!(result, Some(named_vault1.set_as_default())); + + let named_vault2 = repository + .store_vault("vault2", "path2".into(), false) + .await?; + repository.set_as_default("vault2").await?; + let result = repository.get_default_vault().await?; + assert_eq!(result, Some(named_vault2.set_as_default())); + + Ok(()) + } + + #[tokio::test] + async fn test_store_kms_vault() -> Result<()> { + let repository = create_repository().await?; + + // A KMS vault can be created by setting the kms flag to true + let kms = repository.store_vault("kms", "path".into(), true).await?; + let expected = NamedVault::new("kms", "path".into(), false, true); + assert_eq!(kms, expected); + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(VaultsSqlxDatabase::create().await?) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/test_support.rs b/implementations/rust/ockam/ockam_api/src/cli_state/test_support.rs new file mode 100644 index 00000000000..a12176e58d7 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/test_support.rs @@ -0,0 +1,20 @@ +use crate::cli_state::Result; +use crate::cli_state::{random_name, CliState, CliStateError}; +use std::path::PathBuf; + +/// Test support +impl CliState { + /// Return a test CliState with a random root directory + pub async fn test() -> Result { + Self::create(Self::test_dir()?).await + } + + /// Return a random root directory + pub fn test_dir() -> Result { + Ok(home::home_dir() + .ok_or(CliStateError::InvalidPath("$HOME".to_string()))? + .join(".ockam") + .join(".tests") + .join(random_name())) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/traits.rs b/implementations/rust/ockam/ockam_api/src/cli_state/traits.rs deleted file mode 100644 index 3019e9fec9e..00000000000 --- a/implementations/rust/ockam/ockam_api/src/cli_state/traits.rs +++ /dev/null @@ -1,348 +0,0 @@ -use crate::cli_state::{file_stem, CliState, CliStateError}; -use fs2::FileExt; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{async_trait, Error}; -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; - -use super::Result; -pub const DATA_DIR_NAME: &str = "data"; - -/// Represents the directory of a type of state. This directory contains a list of items, uniquely -/// identified by a name, and represented by the same `Item` type. -/// -/// One item can be set as the "default" item, which is used in some CLI commands when no -/// argument is provided for that type of `Item`. -#[async_trait] -pub trait StateDirTrait: Sized + Send + Sync { - type Item: StateItemTrait; - const DEFAULT_FILENAME: &'static str; - const DIR_NAME: &'static str; - const HAS_DATA_DIR: bool; - - fn new(root_path: &Path) -> Self; - - fn default_filename() -> &'static str { - Self::DEFAULT_FILENAME - } - fn build_dir(root_path: &Path) -> PathBuf { - root_path.join(Self::DIR_NAME) - } - fn has_data_dir() -> bool { - Self::HAS_DATA_DIR - } - - /// Load the root configuration - /// and migrate each entry if necessary - async fn init(root_path: &Path) -> Result { - let root = Self::load(root_path)?; - for path in root.list_items_paths()? { - root.migrate(path.as_path()).await?; - } - Ok(root) - } - - /// Do not run any migration by default - async fn migrate(&self, _path: &Path) -> Result<()> { - Ok(()) - } - - fn load(root_path: &Path) -> Result { - Self::create_dirs(root_path)?; - Ok(Self::new(root_path)) - } - - /// Recreate all the state directories - fn reset(&self, root_path: &Path) -> Result { - Self::create_dirs(root_path) - } - - /// Create all the state directories - fn create_dirs(root_path: &Path) -> Result { - let dir = Self::build_dir(root_path); - if Self::has_data_dir() { - std::fs::create_dir_all(dir.join(DATA_DIR_NAME))?; - } else { - std::fs::create_dir_all(&dir)?; - }; - Ok(dir) - } - - fn dir(&self) -> &PathBuf; - fn dir_as_string(&self) -> String { - self.dir().to_string_lossy().to_string() - } - - fn path(&self, name: impl AsRef) -> PathBuf { - self.dir().join(format!("{}.json", name.as_ref())) - } - - fn overwrite( - &self, - name: impl AsRef, - config: <::Item as StateItemTrait>::Config, - ) -> Result { - let path = self.path(&name); - let state = with_lock(&path, || Self::Item::new(path.clone(), config))?; - if !self.default_path()?.exists() { - self.set_default(&name)?; - } - Ok(state) - } - - fn create( - &self, - name: impl AsRef, - config: <::Item as StateItemTrait>::Config, - ) -> Result { - debug!(name = %name.as_ref(), "Creating new config resource"); - if self.exists(&name) { - return Err(CliStateError::AlreadyExists { - resource: Self::default_filename().to_string(), - name: name.as_ref().to_string(), - }); - } - trace!(name = %name.as_ref(), "Creating config resource instance"); - let state = Self::Item::new(self.path(&name), config)?; - if !self.default_path()?.exists() { - self.set_default(&name)?; - } - info!(name = %name.as_ref(), "Created new config resource"); - Ok(state) - } - - fn get(&self, name: impl AsRef) -> Result { - if !self.exists(&name) { - return Err(CliStateError::ResourceNotFound { - resource: Self::default_filename().to_string(), - name: name.as_ref().to_string(), - }); - } - Self::Item::load(self.path(&name)) - } - - fn list(&self) -> Result> { - let mut items = Vec::default(); - for name in self.list_items_names()? { - if let Ok(item) = self.get(name) { - items.push(item); - } - } - Ok(items) - } - - fn list_items_names(&self) -> Result> { - let mut items = Vec::default(); - let iter = std::fs::read_dir(self.dir()).map_err(|e| { - let dir = self.dir().as_path().to_string_lossy(); - error!(%dir, %e, "Unable to read state directory"); - CliStateError::InvalidOperation(format!("Unable to read state from directory {dir}")) - })?; - for entry in iter { - let entry_path = entry?.path(); - if self.is_item_path(&entry_path)? { - items.push(file_stem(&entry_path)?); - } - } - Ok(items) - } - - // If a path has been created with the self.path function - // then we know that the current name is an item name - fn is_item_path(&self, path: &PathBuf) -> Result { - let name = file_stem(path)?; - Ok(path.eq(&self.path(name))) - } - - fn list_items_paths(&self) -> Result> { - let mut items = Vec::default(); - for name in self.list_items_names()? { - let path = self.path(name); - items.push(path); - } - Ok(items) - } - - // TODO: move to StateItemTrait - fn delete(&self, name: impl AsRef) -> Result<()> { - // Retrieve state. If doesn't exist do nothing. - let s = match self.get(&name) { - Ok(project) => project, - Err(CliStateError::ResourceNotFound { .. }) => return Ok(()), - Err(e) => return Err(e), - }; - // If it's the default, remove link - if let Ok(default) = self.default() { - if default.path() == s.path() { - let _ = std::fs::remove_file(self.default_path()?); - } - } - // Remove state data - s.delete() - } - - fn default_path(&self) -> Result { - let root_path = self.dir().parent().expect("Should have parent"); - Ok(CliState::defaults_dir(root_path)?.join(Self::default_filename())) - } - - fn default(&self) -> Result { - let path = std::fs::canonicalize(self.default_path()?)?; - Self::Item::load(path) - } - - fn set_default(&self, name: impl AsRef) -> Result<()> { - debug!(name = %name.as_ref(), "Setting default item"); - if !self.exists(&name) { - return Err(CliStateError::ResourceNotFound { - resource: Self::default_filename().to_string(), - name: name.as_ref().to_string(), - }); - } - let original = self.path(&name); - let link = self.default_path()?; - info!("removing link {:?}", link); - // Remove link if it exists - let _ = std::fs::remove_file(&link); - info!("symlink to {:?}", original); - // Create link to the default item - std::fs::create_dir_all(link.parent().unwrap()) - .map_err(|e| Error::new(Origin::Node, Kind::Io, e))?; - std::os::unix::fs::symlink(original, link)?; - info!(name = %name.as_ref(), "Set default item"); - Ok(()) - } - - fn is_default(&self, name: impl AsRef) -> Result { - if !self.exists(&name) { - return Ok(false); - } - let default_name = { - let path = std::fs::canonicalize(self.default_path()?)?; - file_stem(&path)? - }; - Ok(default_name.eq(name.as_ref())) - } - - fn is_empty(&self) -> Result { - for entry in std::fs::read_dir(self.dir())? { - let name = file_stem(&entry?.path())?; - if self.get(name).is_ok() { - return Ok(false); - } - } - Ok(true) - } - - fn exists(&self, name: impl AsRef) -> bool { - self.path(&name).exists() - } -} - -/// This trait defines the methods to retrieve an item from a state directory. -/// The details of the item are defined in the `Config` type. -#[async_trait] -pub trait StateItemTrait: Sized + Send { - type Config: Serialize + for<'a> Deserialize<'a> + Send; - - /// Create a new item with the given config. - fn new(path: PathBuf, config: Self::Config) -> Result; - - /// Load an item from the given path. - fn load(path: PathBuf) -> Result; - - /// Persist the item to disk after updating the config. - fn persist(&self) -> Result<()> { - with_lock(self.path(), || { - let contents = serde_json::to_string(self.config())?; - std::fs::write(self.path(), contents)?; - Ok(()) - }) - } - - fn delete(&self) -> Result<()> { - with_lock(self.path(), || { - std::fs::remove_file(self.path())?; - Ok(()) - })?; - let _ = std::fs::remove_file(self.path().with_extension("lock")); - Ok(()) - } - - fn path(&self) -> &PathBuf; - fn config(&self) -> &Self::Config; -} - -fn with_lock(path: &Path, f: impl FnOnce() -> Result) -> Result { - let lock_file = std::fs::OpenOptions::new() - .write(true) - .read(true) - .create(true) - .open(path.with_extension("lock"))?; - lock_file.lock_exclusive()?; - let res = f(); - lock_file.unlock()?; - res -} - -#[cfg(test)] -mod tests { - use crate::cli_state::{StateDirTrait, StateItemTrait}; - use std::path::{Path, PathBuf}; - - #[test] - fn test_is_item_path() { - let config = TestConfig::new(Path::new("dir")); - let path = config.path("name"); - assert!(config.is_item_path(&path).unwrap()) - } - - /// Dummy configuration - struct TestConfig { - dir: PathBuf, - } - impl StateDirTrait for TestConfig { - type Item = TestConfigItem; - const DEFAULT_FILENAME: &'static str = "test"; - const DIR_NAME: &'static str = "test"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - let dir = Self::build_dir(root_path); - std::fs::create_dir_all(&dir).unwrap(); - Self { dir } - } - - fn dir(&self) -> &PathBuf { - &self.dir - } - } - - struct TestConfigItem { - path: PathBuf, - config: u32, - } - impl StateItemTrait for TestConfigItem { - type Config = u32; - - fn new(path: PathBuf, config: Self::Config) -> crate::cli_state::Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - Ok(Self { path, config }) - } - - fn load(path: PathBuf) -> crate::cli_state::Result { - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(TestConfigItem { path, config }) - } - - fn path(&self) -> &PathBuf { - &self.path - } - - fn config(&self) -> &Self::Config { - &self.config - } - } -} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts.rs b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts.rs index 67be9d0669f..f8fd2baa3d3 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts.rs @@ -1,101 +1,454 @@ -use super::Result; -use crate::config::cli::TrustContextConfig; use std::fmt::{Display, Formatter}; -use std::path::PathBuf; +use std::sync::Arc; + +use ockam::identity::models::{ChangeHistory, CredentialAndPurposeKey}; +use ockam::identity::{ + AuthorityService, CredentialsMemoryRetriever, Identifier, Identity, RemoteCredentialsRetriever, + RemoteCredentialsRetrieverInfo, SecureChannels, TrustContext, +}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_multiaddr::MultiAddr; +use ockam_transport_tcp::TcpTransport; + +use crate::cli_state::CliState; +use crate::multiaddr_to_route; +use crate::nodes::service::default_address::DefaultAddress; + +use super::Result; + +impl CliState { + pub async fn get_trust_context(&self, name: &str) -> Result { + match self + .trust_contexts_repository() + .await? + .get_trust_context(name) + .await? + { + Some(trust_context) => Ok(trust_context), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("there is no trust context with name {name}"), + ) + .into()), + } + } + + pub async fn get_default_trust_context(&self) -> Result { + match self + .trust_contexts_repository() + .await? + .get_default_trust_context() + .await? + { + Some(trust_context) => Ok(trust_context), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + "there is no default trust context", + ) + .into()), + } + } + + pub async fn get_trust_context_or_default( + &self, + name: &Option, + ) -> Result { + match name { + Some(name) => self.get_trust_context(name).await, + None => self.get_default_trust_context().await, + } + } + + pub async fn retrieve_trust_context( + &self, + trust_context_name: &Option, + project_name: &Option, + authority_identity: &Option, + credential_name: &Option, + ) -> Result> { + match trust_context_name { + Some(name) => Ok(Some(self.get_trust_context(name).await?)), + None => { + let project = match project_name { + Some(name) => self.get_project_by_name(name).await.ok(), + None => self.get_default_project().await.ok(), + }; + match project { + Some(project) => Ok(self.get_trust_context(&project.name).await.ok()), + None => match credential_name { + Some(credential_name) => Ok(Some( + self.create_trust_context( + None, + None, + Some(credential_name.clone()), + authority_identity.clone(), + None, + ) + .await?, + )), + None => Ok(None), + }, + } + } + } + } -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct TrustContextsState { - dir: PathBuf, + pub async fn get_trust_contexts(&self) -> Result> { + Ok(self + .trust_contexts_repository() + .await? + .get_trust_contexts() + .await?) + } + + pub async fn delete_trust_context(&self, name: &str) -> Result<()> { + Ok(self + .trust_contexts_repository() + .await? + .delete_trust_context(name) + .await?) + } + + pub async fn set_default_trust_context(&self, name: &str) -> Result<()> { + Ok(self + .trust_contexts_repository() + .await? + .set_default_trust_context(name) + .await?) + } + + pub async fn create_trust_context( + &self, + name: Option, + trust_context_id: Option, + credential_name: Option, + authority_identity: Option, + authority_route: Option, + ) -> Result { + let credential = match credential_name { + Some(name) => self.get_credential_by_name(&name).await.ok(), + None => None, + }; + let name = name.unwrap_or("default".to_string()); + + // if the authority identity is not defined use the + // authority identity defined on the credential + let authority_identity = match authority_identity.clone() { + Some(identity) => Some(identity), + None => match credential.clone() { + None => None, + Some(credential) => Some(credential.issuer_identity().await?), + }, + }; + + let trust_context_id = trust_context_id + .or_else(|| { + authority_identity + .clone() + .map(|i| i.identifier().to_string()) + }) + .unwrap_or("default".to_string()); + + let trust_context = NamedTrustContext::new( + &name, + &trust_context_id, + credential.map(|c| c.credential_and_purpose_key()), + authority_identity.map(|i| i.change_history().clone()), + authority_route, + ); + + let repository = self.trust_contexts_repository().await?; + repository.store_trust_context(&trust_context).await?; + + // If there is no previous default trust_context set this trust_context as the default + let default_trust_context = repository.get_default_trust_context().await?; + if default_trust_context.is_none() { + repository + .set_default_trust_context(&trust_context.name()) + .await? + }; + + Ok(trust_context) + } } -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct TrustContextState { +/// A NamedTrustContext collects all the data necessary to create a TrustContext +/// under a specific name: +/// +/// Either we can +/// - retrieve a fixed credential +/// - access an authority node to retrieve credentials +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NamedTrustContext { name: String, - path: PathBuf, - config: TrustContextConfig, + trust_context_id: String, + credential: Option, + authority_change_history: Option, + authority_route: Option, } -impl TrustContextState { - pub fn name(&self) -> &str { - &self.name +impl NamedTrustContext { + pub fn new( + name: &str, + trust_context_id: &str, + credential: Option, + authority_identity: Option, + authority_route: Option, + ) -> Self { + Self { + name: name.to_string(), + trust_context_id: trust_context_id.to_string(), + credential, + authority_change_history: authority_identity, + authority_route, + } } } -impl Display for TrustContextState { +impl Display for NamedTrustContext { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Name: {}", self.name)?; + writeln!(f, "Name: {}", self.name())?; Ok(()) } } -mod traits { - use super::*; - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for TrustContextsState { - type Item = TrustContextState; - const DEFAULT_FILENAME: &'static str = "trust_context"; - const DIR_NAME: &'static str = "trust_contexts"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } +impl NamedTrustContext { + pub fn name(&self) -> String { + self.name.clone() + } - fn dir(&self) -> &PathBuf { - &self.dir - } + pub fn trust_context_id(&self) -> String { + self.trust_context_id.to_string() } - impl TrustContextsState { - pub fn read_config_from_path(&self, path: &str) -> Result { - let tcc = match std::fs::read_to_string(path) { - Ok(contents) => { - let mut tc = serde_json::from_str::(&contents)?; - tc.set_path(PathBuf::from(path)); - tc - } - Err(_) => { - let state = self.get(path)?; - let mut tcc = state.config().clone(); - tcc.set_path(state.path().clone()); - tcc - } - }; - Ok(tcc) - } + pub fn credential(&self) -> Option { + self.credential.clone() } - #[async_trait] - impl StateItemTrait for TrustContextState { - type Config = TrustContextConfig; + /// Return the route to the trust context authority if configured + pub fn authority_route(&self) -> Option { + self.authority_route.clone() + } - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - Ok(Self { name, path, config }) - } + /// Return the change history of the trust context authority if configured + pub fn authority_change_history(&self) -> Option { + self.authority_change_history.clone() + } - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { name, path, config }) + /// Return the identity of the trust context authority if configured + pub async fn authority_identity(&self) -> Result> { + match &self.authority_change_history { + Some(change_history) => Ok(Some( + Identity::create_from_change_history(change_history).await?, + )), + None => Ok(None), } + } - fn path(&self) -> &PathBuf { - &self.path - } + /// Return the identifier of the trust context authority if configured + pub async fn authority_identifier(&self) -> Result> { + Ok(self + .authority_identity() + .await? + .map(|i| i.identifier().clone())) + } - fn config(&self) -> &Self::Config { - &self.config + /// Make a TrustContext + /// This requires a transport and secure channels if we need to communicate with an Authority node + pub async fn trust_context( + &self, + tcp_transport: &TcpTransport, + secure_channels: Arc, + ) -> Result { + let authority_identifier = self.authority_identifier().await?; + let authority_service = match ( + self.credential.clone(), + authority_identifier, + self.authority_route.clone(), + ) { + (Some(credential), Some(identifier), _) => { + let credential_retriever = CredentialsMemoryRetriever::new(credential); + Some(AuthorityService::new( + secure_channels.identities().credentials(), + identifier, + Some(Arc::new(credential_retriever)), + )) + } + (None, Some(identifier), Some(route)) => { + let credential_retriever = RemoteCredentialsRetriever::new( + secure_channels.clone(), + RemoteCredentialsRetrieverInfo::new( + identifier.clone(), + multiaddr_to_route(&route, tcp_transport) + .await + .ok_or_else(|| { + Error::new( + Origin::Api, + Kind::Internal, + format!("cannot create a route from the address {route}"), + ) + })? + .route, + DefaultAddress::CREDENTIAL_ISSUER.into(), + ), + ); + Some(AuthorityService::new( + secure_channels.identities().credentials(), + identifier, + Some(Arc::new(credential_retriever)), + )) + } + (None, Some(identifier), None) => Some(AuthorityService::new( + secure_channels.identities().credentials(), + identifier.clone(), + None, + )), + _ => None, + }; + Ok(TrustContext::new( + self.trust_context_id.clone(), + authority_service, + )) + } + + /// Return access data for an authority in order to be able to create + /// a RPC client to that authority and obtain credentials + pub async fn authority(&self) -> Result> { + match ( + self.authority_identifier().await?, + self.authority_route.clone(), + ) { + (Some(identifier), Some(route)) => Ok(Some(Authority::new(identifier, route))), + _ => Ok(None), } } } + +/// Configuration of an authority node +#[derive(Clone)] +pub struct Authority { + identifier: Identifier, + route: MultiAddr, +} + +impl Authority { + pub fn new(identifier: Identifier, route: MultiAddr) -> Self { + Self { identifier, route } + } + + pub fn identifier(&self) -> Identifier { + self.identifier.clone() + } + + pub fn route(&self) -> MultiAddr { + self.route.clone() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::Duration; + + use ockam::identity::models::CredentialAndPurposeKey; + use ockam::identity::models::CredentialSchemaIdentifier; + use ockam::identity::utils::AttributesBuilder; + use ockam::identity::{identities, Identifier, Identities}; + use ockam_core::env::FromString; + + use super::*; + + // There are 3 ways to create a trust context + // - with only an id + // - with a credential + // - with an authority identity + route + #[tokio::test] + async fn test_create_trust_context() -> Result<()> { + let cli = CliState::test().await?; + + // 1. with only an id + let result = cli + .create_trust_context( + Some("trust-context".into()), + Some("1".into()), + None, + None, + None, + ) + .await?; + let expected = NamedTrustContext::new("trust-context", "1", None, None, None); + assert_eq!(result, expected); + + // that trust context is the default one because it is the first created trust context + let result = cli.get_default_trust_context().await?; + assert_eq!(result, expected); + + // 2. with a credential + let identities = identities().await?; + let authority_identifier = identities.identities_creation().create_identity().await?; + let authority = identities.get_identity(&authority_identifier).await?; + let credential = create_credential(identities, &authority_identifier).await?; + cli.store_credential("credential-name", &authority, credential.clone()) + .await?; + let result = cli + .create_trust_context( + Some("trust-context".into()), + None, + Some("credential-name".into()), + None, + None, + ) + .await?; + let expected = NamedTrustContext::new( + "trust-context", + authority.identifier().to_string().as_str(), + Some(credential), + Some(authority.change_history().clone()), + None, + ); + assert_eq!(result, expected); + + // 3. with an authority + let authority_route = MultiAddr::from_string("/dnsaddr/127.0.0.1/tcp/5000/service/api")?; + let result = cli + .create_trust_context( + Some("trust-context".into()), + None, + None, + Some(authority.clone()), + Some(authority_route.clone()), + ) + .await?; + let expected = NamedTrustContext::new( + "trust-context", + authority.identifier().to_string().as_str(), + None, + Some(authority.change_history().clone()), + Some(authority_route), + ); + assert_eq!(result, expected); + Ok(()) + } + + /// HELPERS + pub async fn create_credential( + identities: Arc, + issuer: &Identifier, + ) -> Result { + let subject = identities.identities_creation().create_identity().await?; + + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); + + Ok(identities + .credentials() + .credentials_creation() + .issue_credential(issuer, &subject, attributes, Duration::from_secs(1)) + .await?) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/user_info.rs b/implementations/rust/ockam/ockam_api/src/cli_state/user_info.rs deleted file mode 100644 index 01b923446ef..00000000000 --- a/implementations/rust/ockam/ockam_api/src/cli_state/user_info.rs +++ /dev/null @@ -1,74 +0,0 @@ -use super::Result; -use crate::cloud::enroll::auth0::UserInfo; -use std::fmt::{Display, Formatter}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct UsersInfoState { - dir: PathBuf, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct UserInfoState { - path: PathBuf, - config: UserInfoConfig, -} - -impl Display for UserInfoState { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Email: {}", self.config.email)?; - Ok(()) - } -} - -type UserInfoConfig = UserInfo; - -mod traits { - use super::*; - use crate::cli_state::traits::*; - use ockam_core::async_trait; - use std::path::Path; - - #[async_trait] - impl StateDirTrait for UsersInfoState { - type Item = UserInfoState; - const DEFAULT_FILENAME: &'static str = "user_info"; - const DIR_NAME: &'static str = "users_info"; - const HAS_DATA_DIR: bool = false; - - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } - - fn dir(&self) -> &PathBuf { - &self.dir - } - } - - #[async_trait] - impl StateItemTrait for UserInfoState { - type Config = UserInfoConfig; - - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::write(&path, contents)?; - Ok(Self { path, config }) - } - - fn load(path: PathBuf) -> Result { - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - Ok(Self { path, config }) - } - - fn path(&self) -> &PathBuf { - &self.path - } - - fn config(&self) -> &Self::Config { - &self.config - } - } -} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/users.rs b/implementations/rust/ockam/ockam_api/src/cli_state/users.rs new file mode 100644 index 00000000000..db373f1affc --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/users.rs @@ -0,0 +1,36 @@ +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; + +use crate::cli_state::CliState; +use crate::cli_state::Result; +use crate::cloud::enroll::auth0::UserInfo; + +impl CliState { + pub async fn store_user(&self, user: &UserInfo) -> Result<()> { + let repository = self.users_repository().await?; + let default_user_exists = repository.get_default_user().await?.is_none(); + repository.store_user(user).await?; + + // if this is the first user we store we mark it as the default user + if !default_user_exists { + self.set_default_user(&user.email).await? + } + Ok(()) + } + + pub async fn set_default_user(&self, email: &str) -> Result<()> { + self.users_repository() + .await? + .set_default_user(email) + .await?; + Ok(()) + } + + pub async fn get_default_user(&self) -> Result { + let repository = self.users_repository().await?; + match repository.get_default_user().await? { + Some(user) => Ok(user), + None => Err(Error::new(Origin::Api, Kind::NotFound, "there is no default user").into()), + } + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs index 3851c704296..af1f3ee793d 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs @@ -1,98 +1,234 @@ use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; -use serde::{Deserialize, Serialize}; - -use ockam::identity::Vault; +use ockam::identity::{Identities, Vault}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_node::database::SqlxDatabase; use ockam_vault_aws::AwsSigningVault; -use crate::cli_state::traits::StateItemTrait; -use crate::cli_state::{CliStateError, StateDirTrait, DATA_DIR_NAME}; +use crate::cli_state::{random_name, CliState, Result}; + +/// The methods below support the creation and update of local vaults +/// +/// - by default private keys are stored locally but they can also be stored in a KMS +/// - keys stored locally are stored with other application data in the local database if the default vault is used +/// - any additional vault stores its keys in a separate file +/// +impl CliState { + /// Create a vault with a given name + /// The secrets persisted with this vault are stored under $OCKAM_HOME/vault_name + pub async fn create_named_vault(&self, vault_name: &str) -> Result { + self.create_a_vault(vault_name, false).await + } -use super::Result; + /// Create a KMS vault with a given name. + /// A KMS vault only stores identifiers to secrets physically stored in a KMS like + /// an AWS KMS (the only supported KMS implementation at the moment). + /// + /// The secrets persisted with this vault are stored under $OCKAM_HOME/vault_name + pub async fn create_kms_vault(&self, vault_name: &str) -> Result { + self.create_a_vault(vault_name, true).await + } + + /// Select a different vault to be the default vault + pub async fn set_default_vault(&self, vault_name: &str) -> Result<()> { + Ok(self + .vaults_repository() + .await? + .set_as_default(vault_name) + .await?) + } + + /// Delete an existing vault + pub async fn delete_named_vault(&self, vault_name: &str) -> Result<()> { + let repository = self.vaults_repository().await?; + let vault = repository.get_named_vault(vault_name).await?; + if let Some(vault) = vault { + repository.delete_vault(vault_name).await?; + + // if the vault is stored in a separate file + // remove that file + if vault.path != self.database_path() { + let _ = std::fs::remove_file(vault.path); + } + } + Ok(()) + } -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct VaultsState { - dir: PathBuf, + /// Delete all vaults and their files + pub async fn delete_all_named_vaults(&self) -> Result<()> { + let vaults = self.vaults_repository().await?.get_named_vaults().await?; + for vault in vaults { + self.delete_named_vault(&vault.name()).await?; + } + Ok(()) + } } -impl VaultsState { - pub async fn create_async(&self, name: &str, config: VaultConfig) -> Result { - if self.exists(name) { - return Err(CliStateError::AlreadyExists { - resource: Self::default_filename().to_string(), - name: name.to_string(), - }); +/// The methods below provide an API to query named vaults. +impl CliState { + /// Return all the named vaults + pub async fn get_named_vaults(&self) -> Result> { + Ok(self.vaults_repository().await?.get_named_vaults().await?) + } + + /// Return the vault with a given name + /// and raise an error if the vault is not found + pub async fn get_named_vault(&self, vault_name: &str) -> Result { + let result = self + .vaults_repository() + .await? + .get_named_vault(vault_name) + .await?; + result.ok_or_else(|| { + ockam_core::Error::new( + Origin::Api, + Kind::NotFound, + format!("no vault found with name {vault_name}"), + ) + .into() + }) + } + + /// Return the default vault + /// If it doesn't exist, the vault is created with a random name + pub async fn get_default_named_vault(&self) -> Result { + let result = self.vaults_repository().await?.get_default_vault().await?; + match result { + Some(vault) => Ok(vault), + None => self.create_named_vault(&random_name()).await, } - let state = VaultState::new(self.path(name), config)?; - state.get().await?; - if !self.default_path()?.exists() { - self.set_default(name)?; + } + + /// Return either the default vault or a vault with the given name + /// If the default vault is required and does not exist it is created. + pub async fn get_named_vault_or_default( + &self, + vault_name: &Option, + ) -> Result { + match vault_name { + Some(name) => self.get_named_vault(name).await, + None => self.get_default_named_vault().await, } - Ok(state) } } -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct VaultState { +/// Builder functions +impl CliState { + /// Return an Identities struct using a specific Vault + pub async fn make_identities(&self, vault: Vault) -> Result> { + Ok(Identities::builder() + .await? + .with_vault(vault) + .with_change_history_repository(self.change_history_repository().await?) + .with_identity_attributes_repository(self.identity_attributes_repository().await?) + .with_purpose_keys_repository(self.purpose_keys_repository().await?) + .build()) + } +} + +/// Private functions +impl CliState { + /// Create a vault with the given name and indicate if it is going to be used as a KMS vault + /// The vault path is either + /// - the database path if this is the first created vault (it is set as the default vault) + /// - a file next to the database file, named 'vault_name' + async fn create_a_vault(&self, vault_name: &str, is_kms: bool) -> Result { + let vaults_repository = self.vaults_repository().await?; + + // the first created vault is the default one + let is_default_vault = vaults_repository.get_default_vault().await?.is_none(); + + // if the vault is the default vault we store the data directly in the main database + // otherwise we open a new file with the vault name + let path = if is_default_vault { + self.database_path() + } else { + self.dir().join(vault_name) + }; + + let mut vault = vaults_repository + .store_vault(vault_name, path, is_kms) + .await?; + if is_default_vault { + vaults_repository.set_as_default(vault_name).await?; + vault = vault.set_as_default(); + } + Ok(vault) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct NamedVault { name: String, path: PathBuf, - /// The path to the vault's storage config file, contained in the data directory - data_path: PathBuf, - config: VaultConfig, + is_default: bool, + is_kms: bool, } -impl VaultState { - pub async fn get(&self) -> Result { - if self.config.aws_kms { - let mut vault = Vault::create(); - let aws_vault = Arc::new(AwsSigningVault::create().await?); - vault.identity_vault = aws_vault.clone(); - vault.credential_vault = aws_vault; - - Ok(vault) - } else { - let vault = - Vault::create_with_persistent_storage_path(self.vault_file_path().as_path()) - .await?; - Ok(vault) +impl NamedVault { + /// Create a new named vault + pub fn new(name: &str, path: PathBuf, is_default: bool, is_kms: bool) -> Self { + Self { + name: name.to_string(), + path, + is_default, + is_kms, } } - fn build_data_path(name: &str, path: &Path) -> PathBuf { - path.parent() - .expect("Should have parent") - .join(DATA_DIR_NAME) - .join(format!("{name}-storage.json")) + /// Return the vault name + pub fn name(&self) -> String { + self.name.clone() } - pub fn vault_file_path(&self) -> &PathBuf { - &self.data_path + /// Return the vault path + pub fn path(&self) -> PathBuf { + self.path.clone() } - pub async fn vault(&self) -> Result { - let path = self.vault_file_path().clone(); - let vault = Vault::create_with_persistent_storage_path(path.as_path()).await?; - Ok(vault) + /// Return true if this vault is the default one + pub fn is_default(&self) -> bool { + self.is_default } - pub fn name(&self) -> &str { - &self.name + /// Return a copy of this vault as vault with the is_default flag set to true + pub fn set_as_default(&self) -> NamedVault { + let mut result = self.clone(); + result.is_default = true; + result + } + + /// Return true if this vault is a KMS vault + pub fn is_kms(&self) -> bool { + self.is_kms + } + + pub async fn vault(&self) -> Result { + if self.is_kms { + let mut vault = Vault::create().await?; + let aws_vault = Arc::new(AwsSigningVault::create().await?); + vault.identity_vault = aws_vault.clone(); + vault.credential_vault = aws_vault; + Ok(vault) + } else { + Ok(Vault::create_with_database(self.database().await?)) + } } - pub fn is_aws(&self) -> bool { - self.config.is_aws() + async fn database(&self) -> Result> { + Ok(Arc::new(SqlxDatabase::create(self.path.as_path()).await?)) } } -impl Display for VaultState { +impl Display for NamedVault { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "Name: {}", self.name)?; writeln!( f, "Type: {}", - match self.config.is_aws() { + match self.is_kms { true => "AWS KMS", false => "OCKAM", } @@ -101,117 +237,57 @@ impl Display for VaultState { } } -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Default)] -pub struct VaultConfig { - #[serde(default)] - aws_kms: bool, -} +#[cfg(test)] +mod tests { + use super::*; -impl VaultConfig { - pub fn new(aws_kms: bool) -> Result { - Ok(Self { aws_kms }) - } + #[tokio::test] + async fn test_create_named_vault() -> Result<()> { + let cli = CliState::test().await?; - pub fn is_aws(&self) -> bool { - self.aws_kms - } -} + // create a vault + let named_vault1 = cli.create_named_vault("vault1").await?; -mod traits { - use ockam_core::async_trait; + let result = cli.get_named_vault("vault1").await?; + assert_eq!(result, named_vault1.clone()); - use crate::cli_state::file_stem; - use crate::cli_state::traits::*; + // create another vault + let named_vault2 = cli.create_named_vault("vault2").await?; - use super::*; + let result = cli.get_named_vaults().await?; + assert_eq!(result, vec![named_vault1.clone(), named_vault2.clone()]); - #[async_trait] - impl StateDirTrait for VaultsState { - type Item = VaultState; - const DEFAULT_FILENAME: &'static str = "vault"; - const DIR_NAME: &'static str = "vaults"; - const HAS_DATA_DIR: bool = true; + // the first created vault is the default one + let result = cli.get_default_named_vault().await?; + assert_eq!(result, named_vault1.clone()); - fn new(root_path: &Path) -> Self { - Self { - dir: Self::build_dir(root_path), - } - } + // the default vault can be changed + cli.set_default_vault("vault2").await?; + let result = cli.get_default_named_vault().await?; + assert_eq!(result, named_vault2.set_as_default()); - fn dir(&self) -> &PathBuf { - &self.dir - } + // a vault can be deleted + cli.delete_named_vault("vault2").await?; + let result = cli.get_default_named_vault().await?; + assert_eq!(result, named_vault1.set_as_default()); - fn create( - &self, - _name: impl AsRef, - _config: <::Item as StateItemTrait>::Config, - ) -> Result { - unreachable!() - } + // all the vaults can be deleted + cli.delete_all_named_vaults().await?; + let result = cli.get_named_vaults().await?; + assert!(result.is_empty()); - fn delete(&self, name: impl AsRef) -> Result<()> { - // If doesn't exist do nothing. - if !self.exists(&name) { - return Ok(()); - } - let vault = self.get(&name)?; - // If it's the default, remove link - if let Ok(default) = self.default() { - if default.path == vault.path { - let _ = std::fs::remove_file(self.default_path()?); - } - } - // Remove vault files - vault.delete()?; - Ok(()) - } + Ok(()) } - #[async_trait] - impl StateItemTrait for VaultState { - type Config = VaultConfig; - - fn new(path: PathBuf, config: Self::Config) -> Result { - let contents = serde_json::to_string(&config)?; - std::fs::create_dir_all(path.parent().unwrap())?; - std::fs::write(&path, contents)?; - let name = file_stem(&path)?; - let data_path = VaultState::build_data_path(&name, &path); - Ok(Self { - name, - path, - data_path, - config, - }) - } - - fn load(path: PathBuf) -> Result { - let name = file_stem(&path)?; - let contents = std::fs::read_to_string(&path)?; - let config = serde_json::from_str(&contents)?; - let data_path = VaultState::build_data_path(&name, &path); - Ok(Self { - name, - path, - data_path, - config, - }) - } - - fn delete(&self) -> Result<()> { - std::fs::remove_file(&self.path)?; - std::fs::remove_file(&self.data_path)?; - std::fs::remove_file(self.data_path.with_extension("json.lock"))?; - Ok(()) - } + #[tokio::test] + async fn test_get_default_named_vault() -> Result<()> { + let cli = CliState::test().await?; - fn path(&self) -> &PathBuf { - &self.path - } + // the default vault is always available + let vault = cli.get_default_named_vault().await?; + assert!(vault.is_default()); + assert!(vault.path().starts_with(cli.dir())); - fn config(&self) -> &Self::Config { - &self.config - } + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_api/src/cloud/addon.rs b/implementations/rust/ockam/ockam_api/src/cloud/addon.rs index f8286530590..efd3cc46f8d 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/addon.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/addon.rs @@ -1,12 +1,14 @@ -use crate::cloud::operation::CreateOperationResponse; -use crate::cloud::project::{InfluxDBTokenLeaseManagerConfig, OktaConfig}; -use crate::cloud::Controller; use miette::IntoDiagnostic; use minicbor::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + use ockam_core::api::Request; use ockam_core::async_trait; use ockam_node::Context; -use serde::{Deserialize, Serialize}; + +use crate::cloud::operation::CreateOperationResponse; +use crate::cloud::project::{InfluxDBTokenLeaseManagerConfig, OktaConfig}; +use crate::cloud::Controller; const TARGET: &str = "ockam_api::cloud::addon"; const API_SERVICE: &str = "projects"; @@ -23,7 +25,7 @@ pub struct Addon { pub enabled: bool, } -#[derive(Encode, Decode, Serialize, Deserialize, Debug)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[rustfmt::skip] #[cbor(map)] pub struct ConfluentConfig { @@ -54,7 +56,7 @@ impl ConfluentConfigResponse { } #[cfg(test)] -impl quickcheck::Arbitrary for ConfluentConfigResponse { +impl quickcheck::Arbitrary for ConfluentConfig { fn arbitrary(g: &mut quickcheck::Gen) -> Self { Self { bootstrap_server: String::arbitrary(g), @@ -79,40 +81,40 @@ impl DisableAddon { #[async_trait] pub trait Addons { - async fn list_addons(&self, ctx: &Context, project_id: String) -> miette::Result>; + async fn list_addons(&self, ctx: &Context, project_id: &str) -> miette::Result>; async fn configure_confluent_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: ConfluentConfig, ) -> miette::Result; async fn configure_okta_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: OktaConfig, ) -> miette::Result; async fn configure_influxdb_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: InfluxDBTokenLeaseManagerConfig, ) -> miette::Result; async fn disable_addon( &self, ctx: &Context, - project_id: String, - addon_id: String, + project_id: &str, + addon_id: &str, ) -> miette::Result; } #[async_trait] impl Addons for Controller { - async fn list_addons(&self, ctx: &Context, project_id: String) -> miette::Result> { + async fn list_addons(&self, ctx: &Context, project_id: &str) -> miette::Result> { trace!(target: TARGET, project_id, "listing addons"); let req = Request::get(format!("/v0/{project_id}/addons")); self.secure_client @@ -126,7 +128,7 @@ impl Addons for Controller { async fn configure_confluent_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: ConfluentConfig, ) -> miette::Result { trace!(target: TARGET, project_id, "configuring confluent addon"); @@ -145,7 +147,7 @@ impl Addons for Controller { async fn configure_okta_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: OktaConfig, ) -> miette::Result { trace!(target: TARGET, project_id, "configuring okta addon"); @@ -162,7 +164,7 @@ impl Addons for Controller { async fn configure_influxdb_addon( &self, ctx: &Context, - project_id: String, + project_id: &str, config: InfluxDBTokenLeaseManagerConfig, ) -> miette::Result { // @@ -182,8 +184,8 @@ impl Addons for Controller { async fn disable_addon( &self, ctx: &Context, - project_id: String, - addon_id: String, + project_id: &str, + addon_id: &str, ) -> miette::Result { trace!(target: TARGET, project_id, "disabling addon"); let req = Request::post(format!("/v1/projects/{project_id}/disable_addon")) diff --git a/implementations/rust/ockam/ockam_api/src/cloud/project.rs b/implementations/rust/ockam/ockam_api/src/cloud/project.rs index 8e966fa763f..cf214117bd5 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/project.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/project.rs @@ -6,17 +6,18 @@ use serde::{Deserialize, Serialize}; use tokio_retry::strategy::FixedInterval; use tokio_retry::Retry; -use ockam::identity::Identifier; +use ockam::identity::models::ChangeHistory; +use ockam::identity::{identities, Identifier, Identity}; use ockam_core::api::Request; -use ockam_core::{async_trait, Result}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{async_trait, Error, Result}; use ockam_multiaddr::MultiAddr; use ockam_node::{tokio, Context}; -use crate::cloud::addon::ConfluentConfigResponse; +use crate::cloud::addon::ConfluentConfig; use crate::cloud::operation::Operations; use crate::cloud::share::ShareScope; use crate::cloud::{Controller, ORCHESTRATOR_AWAIT_TIMEOUT}; -use crate::config::lookup::ProjectAuthority; use crate::error::ApiError; use crate::minicbor_url::Url; @@ -60,7 +61,7 @@ pub struct Project { #[cbor(n(12))] #[serde(skip_serializing_if = "Option::is_none")] - pub confluent_config: Option, + pub confluent_config: Option, #[cbor(n(13))] pub version: Option, @@ -80,16 +81,103 @@ pub struct Project { #[rustfmt::skip] pub struct ProjectUserRole { #[n(1)] pub email: String, - #[n(2)] pub id: usize, + #[n(2)] pub id: u64, #[n(3)] pub role: RoleInShare, #[n(4)] pub scope: ShareScope, } impl Project { + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn id(&self) -> String { + self.id.clone() + } + + pub fn identifier(&self) -> Result { + match &self.identity.clone() { + Some(identifier) => Ok(identifier.clone()), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!("no identity has been created for the project {}", self.name), + )), + } + } + + pub fn project_name(&self) -> String { + self.name.clone() + } + pub fn access_route(&self) -> Result { MultiAddr::from_str(&self.access_route).map_err(|e| ApiError::core(e.to_string())) } + pub fn authority_access_route(&self) -> Result { + match &self.authority_access_route { + Some(authority_access_route) => MultiAddr::from_str(authority_access_route) + .map_err(|e| ApiError::core(e.to_string())), + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!( + "no authority access route has been configured for the project {}", + self.name + ), + )), + } + } + + /// Return the decoded authority change history + /// This method does not verify the change history so it does not require to be async + pub fn authority_change_history(&self) -> Result { + match &self.authority_identity { + Some(authority_identity) => { + let decoded = hex::decode(authority_identity.as_bytes()) + .map_err(|e| Error::new(Origin::Api, Kind::NotFound, e.to_string()))?; + Ok(ChangeHistory::import(&decoded)?) + } + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!( + "no authority change history has been configured for the project {}", + self.name + ), + )), + } + } + + /// Return the identifier of the project's authority + pub async fn authority_identifier(&self) -> Result { + Ok(self.authority_identity().await?.identifier().clone()) + } + + /// Return the identity of the project's authority + pub async fn authority_identity(&self) -> Result { + match &self.authority_identity { + Some(authority_identity) => { + let decoded = hex::decode(authority_identity.as_bytes()) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + let identities = identities().await?; + let identifier = identities + .identities_creation() + .import(None, &decoded) + .await?; + Ok(identities.get_identity(&identifier).await?) + } + None => Err(Error::new( + Origin::Api, + Kind::NotFound, + format!( + "no authority identity has been configured for the project {}", + self.name + ), + )), + } + } + pub fn has_admin_with_email(&self, email: &str) -> bool { self.user_roles .iter() @@ -117,18 +205,11 @@ impl Project { ma.to_socket_addr() .map_err(|e| ApiError::core(e.to_string())) } - - /// Return the project authority if there is one defined - pub async fn authority(&self) -> Result> { - ProjectAuthority::from_project(self) - .await - .map_err(|e| ApiError::core(e.to_string())) - } } #[derive(Decode, Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[cbor(map)] -pub struct ProjectVersion { +pub struct OrchestratorVersionInfo { /// The version of the Orchestrator Controller #[cbor(n(1))] pub version: Option, @@ -138,6 +219,16 @@ pub struct ProjectVersion { pub project_version: Option, } +impl OrchestratorVersionInfo { + pub fn version(&self) -> String { + self.version.clone().unwrap_or("N/A".to_string()) + } + + pub fn project_version(&self) -> String { + self.project_version.clone().unwrap_or("N/A".to_string()) + } +} + #[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] #[rustfmt::skip] #[cbor(map)] @@ -244,23 +335,45 @@ pub trait Projects { async fn create_project( &self, ctx: &Context, - space_id: String, - name: String, + space_id: &str, + name: &str, users: Vec, ) -> miette::Result; - async fn get_project(&self, ctx: &Context, project_id: String) -> miette::Result; + async fn get_project(&self, ctx: &Context, project_id: &str) -> miette::Result; + + async fn get_project_by_name( + &self, + ctx: &Context, + project_name: &str, + ) -> miette::Result; + + async fn get_project_by_name_or_default( + &self, + ctx: &Context, + project_name: &Option, + ) -> miette::Result; async fn delete_project( &self, ctx: &Context, - space_id: String, - project_id: String, + space_id: &str, + project_id: &str, ) -> miette::Result<()>; - async fn get_project_version(&self, ctx: &Context) -> miette::Result; + async fn delete_project_by_name( + &self, + ctx: &Context, + space_name: &str, + project_name: &str, + ) -> miette::Result<()>; + + async fn get_orchestrator_version_info( + &self, + ctx: &Context, + ) -> miette::Result; - async fn list_projects(&self, ctx: &Context) -> miette::Result>; + async fn get_projects(&self, ctx: &Context) -> miette::Result>; async fn wait_until_project_is_ready( &self, @@ -269,18 +382,17 @@ pub trait Projects { ) -> miette::Result; } -#[async_trait] -impl Projects for Controller { - async fn create_project( +impl Controller { + pub async fn create_project( &self, ctx: &Context, - space_id: String, - name: String, + space_id: &str, + name: &str, users: Vec, ) -> miette::Result { trace!(target: TARGET, %space_id, project_name = name, "creating project"); let req = Request::post(format!("/v1/spaces/{space_id}/projects")) - .body(CreateProject::new(name, users)); + .body(CreateProject::new(name.to_string(), users)); self.secure_client .ask(ctx, "projects", req) .await @@ -289,7 +401,7 @@ impl Projects for Controller { .into_diagnostic() } - async fn get_project(&self, ctx: &Context, project_id: String) -> miette::Result { + pub async fn get_project(&self, ctx: &Context, project_id: &str) -> miette::Result { trace!(target: TARGET, %project_id, "getting project"); let req = Request::get(format!("/v0/{project_id}")); self.secure_client @@ -300,11 +412,11 @@ impl Projects for Controller { .into_diagnostic() } - async fn delete_project( + pub async fn delete_project( &self, ctx: &Context, - space_id: String, - project_id: String, + space_id: &str, + project_id: &str, ) -> miette::Result<()> { trace!(target: TARGET, %space_id, %project_id, "deleting project"); let req = Request::delete(format!("/v0/{space_id}/{project_id}")); @@ -316,8 +428,11 @@ impl Projects for Controller { .into_diagnostic() } - async fn get_project_version(&self, ctx: &Context) -> miette::Result { - trace!(target: TARGET, "getting project version"); + pub async fn get_orchestrator_version_info( + &self, + ctx: &Context, + ) -> miette::Result { + trace!(target: TARGET, "getting orchestrator version information"); self.secure_client .ask(ctx, "version_info", Request::get("")) .await @@ -326,7 +441,7 @@ impl Projects for Controller { .into_diagnostic() } - async fn list_projects(&self, ctx: &Context) -> miette::Result> { + pub async fn list_projects(&self, ctx: &Context) -> miette::Result> { let req = Request::get("/v0"); self.secure_client .ask(ctx, "projects", req) @@ -336,7 +451,7 @@ impl Projects for Controller { .into_diagnostic() } - async fn wait_until_project_is_ready( + pub async fn wait_until_project_is_ready( &self, ctx: &Context, project: Project, @@ -363,7 +478,7 @@ impl Projects for Controller { .await?; if operation.is_successful() { - self.get_project(ctx, project.id).await + self.get_project(ctx, &project.id).await } else { Err(miette!("Operation failed. Please try again.")) } @@ -446,7 +561,7 @@ mod tests { authority_identity: bool::arbitrary(g) .then(|| hex::encode(>::arbitrary(g))), okta_config: bool::arbitrary(g).then(|| OktaConfig::arbitrary(g)), - confluent_config: bool::arbitrary(g).then(|| ConfluentConfigResponse::arbitrary(g)), + confluent_config: bool::arbitrary(g).then(|| ConfluentConfig::arbitrary(g)), version: Some(String::arbitrary(g)), running: bool::arbitrary(g).then(|| bool::arbitrary(g)), operation_id: bool::arbitrary(g).then(|| String::arbitrary(g)), diff --git a/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs b/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs index 9a7712825e8..6d284faf8d1 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs @@ -1,5 +1,6 @@ use std::str::FromStr; use std::time::Duration; + use tokio::spawn; use ockam::identity::{Identifier, SecureChannel, SecureChannels, SecureClient, DEFAULT_TIMEOUT}; @@ -35,7 +36,7 @@ impl NodeManager { NodeManager::controller_node( &self.tcp_transport, self.secure_channels.clone(), - &self.get_identifier(None).await?, + &self.identifier(), ) .await } @@ -238,10 +239,12 @@ pub struct AuthorityNode { pub(crate) secure_client: SecureClient, pub(crate) tcp_connection: Option<(TcpConnection, Context)>, } + pub struct ProjectNode { pub(crate) secure_client: SecureClient, pub(crate) tcp_connection: Option<(TcpConnection, Context)>, } + pub struct Controller { pub(crate) secure_client: SecureClient, pub(crate) tcp_connection: Option<(TcpConnection, Context)>, diff --git a/implementations/rust/ockam/ockam_api/src/cloud/share/create.rs b/implementations/rust/ockam/ockam_api/src/cloud/share/create.rs index 76ab70eb51e..2bc9eaa836d 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/share/create.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/share/create.rs @@ -3,10 +3,9 @@ use serde::{Deserialize, Serialize}; use ockam_core::Result; -use crate::cli_state::{CliState, StateDirTrait, StateItemTrait}; +use crate::cli_state::{CliState, EnrollmentTicket}; use crate::error::ApiError; -use crate::identity::EnrollmentTicket; -use ockam::identity::{identities, Identifier}; +use ockam::identity::Identifier; use super::{RoleInShare, ShareScope}; @@ -50,23 +49,10 @@ impl CreateServiceInvitation { service_route: S, enrollment_ticket: EnrollmentTicket, ) -> Result { - let node_identifier = cli_state.nodes.get(node_name)?.config().identifier()?; - let project = cli_state.projects.get(&project_name)?.config().clone(); - let project_authority_route = project - .authority_access_route - .ok_or(ApiError::core("Project authority route is missing"))?; - let project_authority_identifier = { - let identity = project - .authority_identity - .ok_or(ApiError::core("Project authority identifier is missing"))?; - let as_hex = hex::decode(identity.as_str()).map_err(|_| { - ApiError::core("Project authority identifier is not a valid hex string") - })?; - identities() - .identities_creation() - .import(None, &as_hex) - .await? - }; + let node_identifier = cli_state.get_node(node_name.as_ref()).await?.identifier(); + let project = cli_state.get_project_by_name(project_name.as_ref()).await?; + let project_authority_route = project.authority_access_route()?; + let project_authority_identifier = project.authority_identifier().await?; // see also: ockam_command::project::ticket let enrollment_ticket = hex::encode( serde_json::to_vec(&enrollment_ticket) @@ -75,14 +61,12 @@ impl CreateServiceInvitation { Ok(CreateServiceInvitation { enrollment_ticket, expires_at, - project_id: project.id.to_string(), + project_id: project.id(), recipient_email: recipient_email.as_ref().to_string(), - project_identity: project - .identity - .ok_or(ApiError::core("Project identity is missing"))?, - project_route: project.access_route, + project_identity: project.identifier()?, + project_route: project.access_route()?.to_string(), project_authority_identity: project_authority_identifier, - project_authority_route, + project_authority_route: project_authority_route.to_string(), shared_node_identity: node_identifier, shared_node_route: service_route.as_ref().to_string(), }) diff --git a/implementations/rust/ockam/ockam_api/src/cloud/share/invitation.rs b/implementations/rust/ockam/ockam_api/src/cloud/share/invitation.rs index 3d262c3b9ad..f9959810631 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/share/invitation.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/share/invitation.rs @@ -1,8 +1,8 @@ use std::{fmt::Display, str::FromStr}; use crate::address::extract_address_value; +use crate::cli_state::EnrollmentTicket; use crate::error::ApiError; -use crate::identity::EnrollmentTicket; use minicbor::{Decode, Encode}; use ockam::identity::Identifier; use serde::{Deserialize, Serialize}; diff --git a/implementations/rust/ockam/ockam_api/src/cloud/space.rs b/implementations/rust/ockam/ockam_api/src/cloud/space.rs index 5c5a67b4a93..d46cf27056c 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/space.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/space.rs @@ -1,14 +1,17 @@ -use crate::cloud::Controller; use miette::IntoDiagnostic; use minicbor::{Decode, Encode}; +use serde::Serialize; + use ockam_core::api::Request; use ockam_core::async_trait; use ockam_node::Context; -use serde::Serialize; + +use crate::cloud::Controller; +use crate::nodes::InMemoryNode; const TARGET: &str = "ockam_api::cloud::space"; -#[derive(Encode, Decode, Serialize, Debug, Clone)] +#[derive(Encode, Decode, Serialize, Debug, Clone, PartialEq, Eq)] #[rustfmt::skip] #[cbor(map)] pub struct Space { @@ -17,6 +20,16 @@ pub struct Space { #[n(3)] pub users: Vec, } +impl Space { + pub fn space_id(&self) -> String { + self.id.clone() + } + + pub fn space_name(&self) -> String { + self.name.clone() + } +} + #[derive(Encode, Decode, Debug)] #[cfg_attr(test, derive(Clone))] #[rustfmt::skip] @@ -37,27 +50,115 @@ pub trait Spaces { async fn create_space( &self, ctx: &Context, - name: String, - users: Vec, + name: &str, + users: Vec<&str>, ) -> miette::Result; - async fn get_space(&self, ctx: &Context, space_id: String) -> miette::Result; + async fn get_space(&self, ctx: &Context, space_id: &str) -> miette::Result; + + async fn get_space_by_name(&self, ctx: &Context, space_name: &str) -> miette::Result; - async fn delete_space(&self, ctx: &Context, space_id: String) -> miette::Result<()>; + async fn delete_space(&self, ctx: &Context, space_id: &str) -> miette::Result<()>; - async fn list_spaces(&self, ctx: &Context) -> miette::Result>; + async fn delete_space_by_name(&self, ctx: &Context, space_name: &str) -> miette::Result<()>; + + async fn get_spaces(&self, ctx: &Context) -> miette::Result>; } #[async_trait] -impl Spaces for Controller { +impl Spaces for InMemoryNode { async fn create_space( &self, ctx: &Context, - name: String, - users: Vec, + name: &str, + users: Vec<&str>, + ) -> miette::Result { + let controller = self.create_controller().await?; + let space = controller.create_space(ctx, name, users).await?; + self.cli_state + .store_space( + &space.id, + &space.name, + space.users.iter().map(|u| u.as_ref()).collect(), + ) + .await?; + Ok(space) + } + + async fn get_space(&self, ctx: &Context, space_id: &str) -> miette::Result { + let controller = self.create_controller().await?; + let space = controller.get_space(ctx, space_id).await?; + self.cli_state + .store_space( + &space.id, + &space.name, + space.users.iter().map(|u| u.as_ref()).collect(), + ) + .await?; + Ok(space) + } + + async fn get_space_by_name(&self, ctx: &Context, space_name: &str) -> miette::Result { + let space_id = self + .cli_state + .get_space_by_name(space_name) + .await? + .space_id(); + self.get_space(ctx, &space_id).await + } + + async fn delete_space(&self, ctx: &Context, space_id: &str) -> miette::Result<()> { + let controller = self.create_controller().await?; + controller.delete_space(ctx, space_id).await?; + self.cli_state.delete_space(space_id).await?; + Ok(()) + } + + async fn delete_space_by_name(&self, ctx: &Context, space_name: &str) -> miette::Result<()> { + let space_id = self + .cli_state + .get_space_by_name(space_name) + .await? + .space_id(); + self.delete_space(ctx, &space_id).await + } + + async fn get_spaces(&self, ctx: &Context) -> miette::Result> { + let controller = self.create_controller().await?; + let spaces = controller.list_spaces(ctx).await?; + let default_space = self.cli_state.get_default_space().await.ok(); + for space in &spaces { + self.cli_state + .store_space( + &space.id, + &space.name, + space.users.iter().map(|u| u.as_ref()).collect(), + ) + .await?; + + // make sure that an existing space marked as default is still marked as default + if let Some(default_space) = &default_space { + if space.id == default_space.id { + self.cli_state.set_space_as_default(&space.id).await?; + }; + } + } + Ok(spaces) + } +} + +impl Controller { + pub async fn create_space( + &self, + ctx: &Context, + name: &str, + users: Vec<&str>, ) -> miette::Result { trace!(target: TARGET, space = %name, "creating space"); - let req = Request::post("/v0/").body(CreateSpace::new(name, users)); + let req = Request::post("/v0/").body(CreateSpace::new( + name.into(), + users.iter().map(|u| u.to_string()).collect(), + )); self.secure_client .ask(ctx, "spaces", req) .await @@ -66,7 +167,7 @@ impl Spaces for Controller { .into_diagnostic() } - async fn get_space(&self, ctx: &Context, space_id: String) -> miette::Result { + pub async fn get_space(&self, ctx: &Context, space_id: &str) -> miette::Result { trace!(target: TARGET, space = %space_id, "getting space"); let req = Request::get(format!("/v0/{space_id}")); self.secure_client @@ -77,7 +178,7 @@ impl Spaces for Controller { .into_diagnostic() } - async fn delete_space(&self, ctx: &Context, space_id: String) -> miette::Result<()> { + pub async fn delete_space(&self, ctx: &Context, space_id: &str) -> miette::Result<()> { trace!(target: TARGET, space = %space_id, "deleting space"); let req = Request::delete(format!("/v0/{space_id}")); self.secure_client @@ -88,7 +189,7 @@ impl Spaces for Controller { .into_diagnostic() } - async fn list_spaces(&self, ctx: &Context) -> miette::Result> { + pub async fn list_spaces(&self, ctx: &Context) -> miette::Result> { trace!(target: TARGET, "listing spaces"); self.secure_client .ask(ctx, "spaces", Request::get("/v0/")) diff --git a/implementations/rust/ockam/ockam_api/src/config/cli.rs b/implementations/rust/ockam/ockam_api/src/config/cli.rs index 42210817950..8b137891791 100644 --- a/implementations/rust/ockam/ockam_api/src/config/cli.rs +++ b/implementations/rust/ockam/ockam_api/src/config/cli.rs @@ -1,386 +1 @@ -//! Configuration files used by the ockam CLI -use crate::cli_state::{CliStateError, CredentialState, StateItemTrait}; -use crate::cloud::project::Project; -use crate::config::{lookup::ConfigLookup, ConfigValues}; -use crate::error::ApiError; -use crate::{cli_state, multiaddr_to_transport_route, DefaultAddress, HexByteVec}; -use ockam::identity::{ - identities, AuthorityService, CredentialsMemoryRetriever, CredentialsRetriever, Identifier, - Identities, RemoteCredentialsRetriever, RemoteCredentialsRetrieverInfo, SecureChannels, - TrustContext, -}; -use ockam_core::compat::sync::Arc; -use ockam_core::{Result, Route}; -use ockam_multiaddr::MultiAddr; -use ockam_transport_tcp::TcpTransport; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::str::FromStr; - -use super::lookup::ProjectLookup; - -/// The main ockam CLI configuration -/// -/// Used to determine CLI runtime behaviour and index existing nodes -/// on a system. -/// -/// ## Updates -/// -/// This configuration is read and updated by the user-facing `ockam` -/// CLI. Furthermore the data is only relevant for user-facing -/// `ockam` CLI instances. As such writes to this config don't have -/// to be synchronised to detached consumers. -/// -/// ## Legacy status -/// It's maintained for backwards compatibility with the legacy CLI -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LegacyCliConfig { - /// We keep track of the project directories at runtime but don't - /// persist this data to the configuration - #[serde(skip)] - pub dir: Option, - #[serde(default = "default_lookup")] - pub lookup: ConfigLookup, -} - -fn default_lookup() -> ConfigLookup { - ConfigLookup::default() -} - -impl ConfigValues for LegacyCliConfig { - fn default_values() -> Self { - Self { - dir: Some(Self::dir()), - lookup: default_lookup(), - } - } -} - -impl LegacyCliConfig { - /// Determine the default storage location for the ockam config - pub fn dir() -> PathBuf { - cli_state::CliState::default_dir().unwrap() - } - - /// This function could be zero-copy if we kept the lock on the - /// backing store for as long as we needed it. Because this may - /// have unwanted side-effects, instead we eagerly copy data here. - /// This may be optimised in the future! - pub fn lookup(&self) -> &ConfigLookup { - &self.lookup - } -} - -/// A configuration struct to serialize and deserialize a trust context -/// used within the ockam CLI and ockam node -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct TrustContextConfig { - id: String, - authority: Option, - path: Option, -} - -impl TrustContextConfig { - pub fn new(id: String, authority: Option) -> Self { - Self { - id, - authority, - path: None, - } - } - - pub fn id(&self) -> &str { - &self.id - } - - pub fn path(&self) -> Option<&PathBuf> { - self.path.as_ref() - } - - pub fn set_path(&mut self, path: PathBuf) { - self.path = Some(path); - } - - pub fn authority(&self) -> Result<&TrustAuthorityConfig> { - self.authority - .as_ref() - .ok_or_else(|| ApiError::core("Missing authority on trust context config")) - } - - pub async fn to_trust_context( - &self, - secure_channels: Arc, - tcp_transport: Option, - ) -> Result { - let authority = if let Some(authority_config) = self.authority.as_ref() { - let identifier = authority_config.identifier().await?; - let credential_retriever = - if let Some(retriever_type) = &authority_config.own_credential { - Some( - retriever_type - .to_credential_retriever(secure_channels.clone(), tcp_transport) - .await?, - ) - } else { - None - }; - - Some(AuthorityService::new( - secure_channels.identities().credentials(), - identifier, - credential_retriever, - )) - } else { - None - }; - - Ok(TrustContext::new(self.id.to_string(), authority)) - } - - pub fn from_authority_identity( - authority_identity: &str, - credential: Option, - ) -> Result { - let own_cred = credential.map(CredentialRetrieverConfig::FromPath); - let trust_context = TrustContextConfig::new( - authority_identity.to_string(), - Some(TrustAuthorityConfig::new( - authority_identity.to_string(), - own_cred, - )), - ); - - Ok(trust_context) - } -} - -impl TryFrom for TrustContextConfig { - type Error = CliStateError; - - fn try_from(state: CredentialState) -> std::result::Result { - let issuer = hex::encode(&state.config().encoded_issuer_change_history); - let identifier = state.config().issuer_identifier.clone().to_string(); - let retriever = CredentialRetrieverConfig::FromPath(state); - let authority = TrustAuthorityConfig::new(issuer, Some(retriever)); - Ok(TrustContextConfig::new(identifier, Some(authority))) - } -} - -impl TryFrom for TrustContextConfig { - type Error = CliStateError; - - fn try_from(project_info: Project) -> std::result::Result { - let authority = match ( - &project_info.authority_access_route, - &project_info.authority_identity, - ) { - (Some(route), Some(identity)) => { - let authority_route = MultiAddr::from_str(route) - .map_err(|_| ApiError::core("incorrect multi address"))?; - let retriever = CredentialRetrieverConfig::FromCredentialIssuer( - CredentialIssuerConfig::new(identity.to_string(), authority_route), - ); - let authority = TrustAuthorityConfig::new(identity.to_string(), Some(retriever)); - Some(authority) - } - _ => None, - }; - - Ok(TrustContextConfig::new(project_info.id, authority)) - } -} - -impl TryFrom for TrustContextConfig { - type Error = ApiError; - - fn try_from( - project_lookup: ProjectLookup, - ) -> std::result::Result { - let proj_auth = project_lookup - .authority - .as_ref() - .expect("Project lookup is missing authority"); - let public_identity = hex::encode(proj_auth.identity()); - let authority = { - let retriever = CredentialRetrieverConfig::FromCredentialIssuer( - CredentialIssuerConfig::new(public_identity.clone(), proj_auth.address().clone()), - ); - let authority = TrustAuthorityConfig::new(public_identity, Some(retriever)); - Some(authority) - }; - - Ok(TrustContextConfig::new( - project_lookup.id.clone(), - authority, - )) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TrustAuthorityConfig { - identity: String, - own_credential: Option, -} - -impl TrustAuthorityConfig { - pub fn new(identity: String, own_credential: Option) -> Self { - Self { - identity, - own_credential, - } - } - - pub fn identity_str(&self) -> &str { - &self.identity - } - - pub async fn identifier(&self) -> Result { - identities() - .identities_creation() - .import( - None, - &hex::decode(&self.identity) - .map_err(|_| ApiError::core("unable to decode authority identity"))?, - ) - .await - } - - pub fn own_credential(&self) -> Result<&CredentialRetrieverConfig> { - self.own_credential - .as_ref() - .ok_or_else(|| ApiError::core("Missing own credential on trust authority config")) - } -} - -/// Type of credential retriever -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub enum CredentialRetrieverConfig { - /// Credential is stored in memory - FromMemory(Vec), - /// Path to credential file - FromPath(CredentialState), - /// MultiAddr to Credential Issuer - FromCredentialIssuer(CredentialIssuerConfig), -} - -impl CredentialRetrieverConfig { - async fn to_credential_retriever( - &self, - secure_channels: Arc, - tcp_transport: Option, - ) -> Result> { - match self { - CredentialRetrieverConfig::FromMemory(credential) => Ok(Arc::new( - CredentialsMemoryRetriever::new(minicbor::decode(credential)?), - )), - CredentialRetrieverConfig::FromPath(state) => Ok(Arc::new( - CredentialsMemoryRetriever::new(state.config().credential()?), - )), - CredentialRetrieverConfig::FromCredentialIssuer(issuer_config) => { - let _ = tcp_transport.ok_or_else(|| ApiError::core("TCP Transport was not provided when credential retriever was defined as an issuer."))?; - let credential_issuer_info = RemoteCredentialsRetrieverInfo::new( - issuer_config.resolve_identity().await?, - issuer_config.resolve_route().await?, - DefaultAddress::CREDENTIAL_ISSUER.into(), - ); - - Ok(Arc::new(RemoteCredentialsRetriever::new( - secure_channels, - credential_issuer_info, - ))) - } - } - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct AuthoritiesConfig { - authorities: BTreeMap, -} - -impl AuthoritiesConfig { - pub fn add_authority(&mut self, i: Identifier, a: Authority) { - self.authorities.insert(i, a); - } - - pub fn authorities(&self) -> impl Iterator { - self.authorities.iter() - } - - pub async fn to_identities(&self, identities: Arc) -> Result> { - let mut v = Vec::new(); - for a in self.authorities.values() { - v.push( - identities - .identities_creation() - .import(None, a.identity.as_slice()) - .await?, - ) - } - Ok(v) - } -} - -impl ConfigValues for AuthoritiesConfig { - fn default_values() -> Self { - Self::default() - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct Authority { - identity: HexByteVec, - access: MultiAddr, -} - -impl Authority { - pub fn new(identity: Vec, addr: MultiAddr) -> Self { - Self { - identity: identity.into(), - access: addr, - } - } - - pub fn identity(&self) -> &[u8] { - self.identity.as_slice() - } - - pub fn access_route(&self) -> &MultiAddr { - &self.access - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct CredentialIssuerConfig { - pub identity: String, - pub multiaddr: MultiAddr, -} - -impl CredentialIssuerConfig { - pub fn new(encoded_identity: String, multiaddr: MultiAddr) -> CredentialIssuerConfig { - CredentialIssuerConfig { - identity: encoded_identity, - multiaddr, - } - } - - async fn resolve_route(&self) -> Result { - let Some(route) = multiaddr_to_transport_route(&self.multiaddr) else { - let err_msg = format!("Invalid route within trust context: {}", &self.multiaddr); - error!("{err_msg}"); - return Err(ApiError::core(&err_msg)); - }; - Ok(route) - } - - async fn resolve_identity(&self) -> Result { - let encoded = - hex::decode(&self.identity).map_err(|_| ApiError::core("Invalid project authority"))?; - identities() - .identities_creation() - .import(None, &encoded) - .await - } -} diff --git a/implementations/rust/ockam/ockam_api/src/config/lookup.rs b/implementations/rust/ockam/ockam_api/src/config/lookup.rs index 67ac8e4e38f..96312831c1c 100644 --- a/implementations/rust/ockam/ockam_api/src/config/lookup.rs +++ b/implementations/rust/ockam/ockam_api/src/config/lookup.rs @@ -1,14 +1,8 @@ -use crate::cli_state::{ProjectState, StateItemTrait}; -use crate::cloud::project::{OktaAuth0, Project}; -use crate::error::ApiError; -use bytes::Bytes; -use miette::WrapErr; -use ockam::identity::{identities, Identifier}; use ockam_core::compat::collections::VecDeque; +use ockam_multiaddr::proto::{DnsAddr, Ip4, Ip6, Tcp}; use ockam_multiaddr::MultiAddr; use serde::{Deserialize, Serialize}; use std::{ - collections::BTreeMap, fmt, net::{SocketAddr, SocketAddrV4, SocketAddrV6}, str::FromStr, @@ -22,110 +16,7 @@ pub struct LookupMeta { pub type Name = String; -/// A generic lookup mechanism for configuration values -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ConfigLookup { - #[serde(flatten)] - pub map: BTreeMap, -} - -impl Default for ConfigLookup { - fn default() -> Self { - Self::new() - } -} - -impl ConfigLookup { - pub fn new() -> Self { - Self { - map: Default::default(), - } - } - - pub fn spaces(&self) -> impl Iterator + '_ { - self.map.iter().filter_map(|(k, v)| { - if let LookupValue::Space(p) = v { - let name = k.strip_prefix("/space/").unwrap_or(k).to_string(); - Some((name, p.clone())) - } else { - None - } - }) - } - - pub fn set_space(&mut self, id: &str, name: &str) { - self.map.insert( - format!("/space/{name}"), - LookupValue::Space(SpaceLookup { id: id.to_string() }), - ); - } - - pub fn get_space(&self, name: &str) -> Option<&SpaceLookup> { - self.map - .get(&format!("/space/{name}")) - .and_then(|value| match value { - LookupValue::Space(space) => Some(space), - _ => None, - }) - } - - pub fn remove_space(&mut self, name: &str) -> Option { - self.map.remove(&format!("/space/{name}")) - } - - pub fn remove_spaces(&mut self) { - self.map.retain(|k, _| !k.starts_with("/space/")); - } - - /// Store a project route and identifier as lookup - pub fn set_project(&mut self, name: String, proj: ProjectLookup) { - self.map - .insert(format!("/project/{name}"), LookupValue::Project(proj)); - } - - pub fn get_project(&self, name: &str) -> Option<&ProjectLookup> { - self.map - .get(&format!("/project/{name}")) - .and_then(|value| match value { - LookupValue::Project(project) => Some(project), - _ => None, - }) - } - - pub fn remove_project(&mut self, name: &str) -> Option { - self.map.remove(&format!("/project/{name}")) - } - - pub fn remove_projects(&mut self) { - self.map.retain(|k, _| !k.starts_with("/project/")); - } - - pub fn has_unresolved_projects(&self, meta: &LookupMeta) -> bool { - meta.project - .iter() - .any(|name| self.get_project(name).is_none()) - } - - pub fn projects(&self) -> impl Iterator + '_ { - self.map.iter().filter_map(|(k, v)| { - if let LookupValue::Project(p) = v { - let name = k.strip_prefix("/project/").unwrap_or(k).to_string(); - Some((name, p.clone())) - } else { - None - } - }) - } -} - -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum LookupValue { - Address(InternetAddress), - Space(SpaceLookup), - Project(ProjectLookup), -} - +/// A generic lookup /// An internet address abstraction (v6/v4/dns) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum InternetAddress { @@ -137,6 +28,19 @@ pub enum InternetAddress { V6(SocketAddrV6), } +impl InternetAddress { + pub fn multi_addr(&self) -> ockam_core::Result { + let mut m = MultiAddr::default(); + match self { + InternetAddress::Dns(dns, _) => m.push_back(DnsAddr::new(dns))?, + InternetAddress::V4(v4) => m.push_back(Ip4(*v4.ip()))?, + InternetAddress::V6(v6) => m.push_back(Ip6(*v6.ip()))?, + } + m.push_back(Tcp(self.port()))?; + Ok(m) + } +} + impl Default for InternetAddress { fn default() -> Self { InternetAddress::Dns("localhost".to_string(), 6252) @@ -201,115 +105,3 @@ impl From for InternetAddress { } } } - -/// Represents a remote Ockam space lookup -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct SpaceLookup { - /// Identifier of this space - pub id: String, -} - -/// Represents a remote Ockam project lookup -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ProjectLookup { - /// How to reach the node hosting this project - pub node_route: Option, - /// Identifier of this project - pub id: String, - /// Name of this project within - pub name: String, - /// Identifier of the IDENTITY of the project (for secure-channel) - pub identity_id: Option, - /// Project authority information. - pub authority: Option, - /// OktaAuth0 information. - pub okta: Option, -} - -impl ProjectLookup { - pub async fn from_project(project: &Project) -> ockam_core::Result { - let node_route: MultiAddr = project.access_route.as_str().try_into()?; - let pid = project - .identity - .as_ref() - .ok_or_else(|| ApiError::message("Project should have identity set"))?; - let authority = project.authority().await?; - let okta = project.okta_config.as_ref().map(|o| OktaAuth0 { - tenant_base_url: o.tenant_base_url.clone(), - client_id: o.client_id.to_string(), - certificate: o.certificate.to_string(), - }); - - Ok(ProjectLookup { - node_route: Some(node_route), - id: project.id.to_string(), - name: project.name.to_string(), - identity_id: Some(pid.clone()), - authority, - okta, - }) - } - - pub async fn from_state(projects: Vec) -> miette::Result> { - let mut lookups = BTreeMap::new(); - for p in projects { - let l = ProjectLookup::from_project(p.config()) - .await - .context("Failed to read project configuration")?; - lookups.insert(l.name.clone(), l); - } - Ok(lookups) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct ProjectAuthority { - id: Identifier, - address: MultiAddr, - identity: Bytes, -} - -impl ProjectAuthority { - pub fn new(id: Identifier, addr: MultiAddr, identity: Vec) -> Self { - Self { - id, - address: addr, - identity: identity.into(), - } - } - - pub async fn from_project(project: &Project) -> ockam_core::Result, ApiError> { - Self::from_raw(&project.authority_access_route, &project.authority_identity).await - } - - pub async fn from_raw( - route: &Option, - identity: &Option, - ) -> ockam_core::Result, ApiError> { - if let Some(r) = route { - let rte = MultiAddr::try_from(r.to_string().as_str())?; - let a = identity - .as_ref() - .ok_or_else(|| ApiError::message("Identity is not set"))? - .to_string(); - let a = hex::decode(a.as_str()) - .map_err(|_| ApiError::message("Invalid project authority"))?; - let p = identities().identities_creation().import(None, &a).await?; - Ok(Some(ProjectAuthority::new(p.clone(), rte, a))) - } else { - Ok(None) - } - } - - pub fn identity(&self) -> &[u8] { - &self.identity - } - - pub fn identity_id(&self) -> &Identifier { - &self.id - } - - pub fn address(&self) -> &MultiAddr { - &self.address - } -} diff --git a/implementations/rust/ockam/ockam_api/src/config/mod.rs b/implementations/rust/ockam/ockam_api/src/config/mod.rs index 77bd1ba1ba4..6e35d432c9d 100644 --- a/implementations/rust/ockam/ockam_api/src/config/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/config/mod.rs @@ -12,7 +12,6 @@ use serde::Serialize; use crate::config::atomic::AtomicUpdater; pub mod atomic; -pub mod cli; pub mod lookup; pub trait ConfigValues: Serialize + DeserializeOwned { diff --git a/implementations/rust/ockam/ockam_api/src/enroll/enrollment.rs b/implementations/rust/ockam/ockam_api/src/enroll/enrollment.rs index 3ff9283de83..961d196be79 100644 --- a/implementations/rust/ockam/ockam_api/src/enroll/enrollment.rs +++ b/implementations/rust/ockam/ockam_api/src/enroll/enrollment.rs @@ -1,6 +1,6 @@ use crate::cloud::enroll::auth0::{AuthenticateOidcToken, OidcToken}; use crate::cloud::HasSecureClient; -use crate::DefaultAddress; +use crate::nodes::service::default_address::DefaultAddress; use miette::IntoDiagnostic; use ockam::identity::models::CredentialAndPurposeKey; use ockam::identity::{OneTimeCode, SecureClient}; diff --git a/implementations/rust/ockam/ockam_api/src/identity.rs b/implementations/rust/ockam/ockam_api/src/identity.rs deleted file mode 100644 index b1ac194916e..00000000000 --- a/implementations/rust/ockam/ockam_api/src/identity.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod enrollment_ticket; - -pub use enrollment_ticket::*; diff --git a/implementations/rust/ockam/ockam_api/src/identity/enrollment_ticket.rs b/implementations/rust/ockam/ockam_api/src/identity/enrollment_ticket.rs deleted file mode 100644 index 6867170fc10..00000000000 --- a/implementations/rust/ockam/ockam_api/src/identity/enrollment_ticket.rs +++ /dev/null @@ -1,33 +0,0 @@ -use ockam::identity::OneTimeCode; -use ockam_core::Result; -use serde::{Deserialize, Serialize}; - -use crate::config::{cli::TrustContextConfig, lookup::ProjectLookup}; -use crate::error::ApiError; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EnrollmentTicket { - pub one_time_code: OneTimeCode, - pub project: Option, - pub trust_context: Option, -} - -impl EnrollmentTicket { - pub fn new( - one_time_code: OneTimeCode, - project: Option, - trust_context: Option, - ) -> Self { - Self { - one_time_code, - project, - trust_context, - } - } - - pub fn hex_encoded(&self) -> Result { - let serialized = serde_json::to_vec(&self) - .map_err(|_err| ApiError::core("Failed to authenticate with Okta"))?; - Ok(hex::encode(serialized)) - } -} diff --git a/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/interceptor_listener.rs b/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/interceptor_listener.rs index 833ad0a5a58..ca35bea7184 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/interceptor_listener.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/interceptor_listener.rs @@ -48,7 +48,9 @@ impl OutletManagerService { let worker = OutletManagerService { outlet_controller: KafkaOutletController::new(), incoming_access_control: Arc::new(AbacAccessControl::create( - secure_channels.identities().repository(), + secure_channels + .identities() + .identity_attributes_repository(), TRUST_CONTEXT_ID_UTF8, trust_context_id, )), diff --git a/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/prefix_relay.rs b/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/prefix_relay.rs index cb1be4d6cb1..2699eb7769c 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/prefix_relay.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/outlet_service/prefix_relay.rs @@ -1,12 +1,13 @@ -use crate::kafka::KAFKA_OUTLET_CONSUMERS; - -use crate::DefaultAddress; use core::str::from_utf8; + use ockam::{Context, Result, Routed, Worker}; use ockam_core::errcode::{Kind, Origin}; use ockam_core::flow_control::FlowControlId; use ockam_core::{Address, AllowAll, AllowOnwardAddress}; +use crate::kafka::KAFKA_OUTLET_CONSUMERS; +use crate::nodes::service::default_address::DefaultAddress; + /// This service applies a prefix to the provided static forwarding address. /// This service was created mainly to keep full compatibility with the existing /// erlang implementation. @@ -14,6 +15,7 @@ pub struct PrefixRelayService { prefix: String, secure_channel_listener_flow_control_id: FlowControlId, } + impl PrefixRelayService { pub async fn create( context: &Context, diff --git a/implementations/rust/ockam/ockam_api/src/kafka/portal_worker.rs b/implementations/rust/ockam/ockam_api/src/kafka/portal_worker.rs index 070809cb7b8..8ddeef6b8fb 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/portal_worker.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/portal_worker.rs @@ -741,7 +741,7 @@ mod test { PortRange::new(0, 0).unwrap(), ); - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await.unwrap(); let secure_channel_controller = KafkaSecureChannelControllerImpl::new( secure_channels, ConsumerNodeAddr::Relay(MultiAddr::default()), diff --git a/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs b/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs index f675db9fa3f..650915ec062 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs @@ -1,12 +1,5 @@ -use crate::kafka::KAFKA_OUTLET_CONSUMERS; -use crate::nodes::models::relay::{CreateRelay, RelayInfo}; -use crate::nodes::models::secure_channel::{ - CreateSecureChannelRequest, CreateSecureChannelResponse, DeleteSecureChannelRequest, - DeleteSecureChannelResponse, -}; -use crate::nodes::NODEMANAGER_ADDR; -use crate::DefaultAddress; use minicbor::Decoder; + use ockam::identity::{ DecryptionRequest, DecryptionResponse, EncryptionRequest, EncryptionResponse, SecureChannelRegistryEntry, SecureChannels, TRUST_CONTEXT_ID_UTF8, @@ -23,6 +16,15 @@ use ockam_node::compat::tokio::sync::Mutex; use ockam_node::compat::tokio::sync::MutexGuard; use ockam_node::Context; +use crate::kafka::KAFKA_OUTLET_CONSUMERS; +use crate::nodes::models::relay::{CreateRelay, RelayInfo}; +use crate::nodes::models::secure_channel::{ + CreateSecureChannelRequest, CreateSecureChannelResponse, DeleteSecureChannelRequest, + DeleteSecureChannelResponse, +}; +use crate::nodes::service::default_address::DefaultAddress; +use crate::nodes::NODEMANAGER_ADDR; + pub(crate) struct KafkaEncryptedContent { /// The encrypted content pub(crate) content: Vec, @@ -155,6 +157,7 @@ pub(crate) enum ConsumerNodeAddr { } type TopicPartition = (String, i32); + struct InnerSecureChannelControllerImpl { // we identity the secure channel instance by using the decryptor of the consumer // which is known to both parties @@ -202,7 +205,9 @@ impl KafkaSecureChannelControllerImpl { trust_context_id: String, ) -> KafkaSecureChannelControllerImpl { let access_control = AbacAccessControl::create( - secure_channels.identities().repository(), + secure_channels + .identities() + .identity_attributes_repository(), TRUST_CONTEXT_ID_UTF8, &trust_context_id, ); @@ -412,14 +417,17 @@ impl KafkaSecureChannelControllerImpl { Err(Error::new( Origin::Transport, Kind::Invalid, - "unauthorized secure channel for consumer", + format!( + "unauthorized secure channel for consumer with identifier {}", + entry.their_id() + ), )) } } else { Err(Error::new( Origin::Transport, Kind::Unknown, - "cannot find secure channel entry", + format!("cannot find secure channel entry {producer_encryptor_address}"), )) } } diff --git a/implementations/rust/ockam/ockam_api/src/lib.rs b/implementations/rust/ockam/ockam_api/src/lib.rs index 4a94a61d6d3..a99135fbf8c 100644 --- a/implementations/rust/ockam/ockam_api/src/lib.rs +++ b/implementations/rust/ockam/ockam_api/src/lib.rs @@ -3,126 +3,22 @@ //! //! # Configuration //! -//! A `NodeManager` maintains its configuration as a list of directories and files stored under +//! A `NodeManager` maintains its database and log files on disk in //! the `OCKAM_HOME` directory (`~/.ockam`) by default: //! ```shell //! root -//! ├─ credentials -//! │ ├─ c1.json -//! │ ├─ c2.json -//! │ └─ ... -//! ├─ defaults -//! │ ├── credential -> ... -//! │ ├── identity -> ... -//! │ ├── node -> ... -//! │ └── vault -> ... -//! ├─ identities -//! │ ├─ data -//! │ │ ├─ authenticated-storage.lmdb -//! │ │ └─ authenticated-storage.lmdb-lock -//! │ ├─ identity1.json -//! │ ├─ identity2.json -//! │ └─ ... +//! ├─ database.sqlite //! ├─ nodes //! │ ├─ node1 -//! │ │ ├─ default_identity -> ... -//! │ │ ├─ default_vault -> ... -//! │ │ ├─ policies-storage.lmdb -//! │ │ ├─ policies-storage.lmdb-lock -//! │ │ ├─ setup.json //! │ │ ├─ stderr.log //! │ │ ├─ stdout.log -//! │ │ └─ version.log //! │ ├─ node2 //! │ └─ ... -//! ├─ projects -//! │ └─ default.json -//! ├─ trust_contexts -//! │ └─ default.json -//! └─ vaults -//! ├─ vault1.json -//! ├─ vault2.json -//! ├─ ... -//! └─ data -//! ├─ vault1.lmdb -//! ├─ vault1.lmdb-lock -//! ├─ vault2.lmdb -//! ├─ vault2.lmdb-lock -//! └─ ... //! ``` -//! # `credentials` -//! -//! Each file stored under the `credentials` directory contains the credential for a given identity. -//! Those files are created with the `ockam credential store` command. They are then read during the creation of -//! a secure channel to send the credentials to the other party -//! -//! # `defaults` -//! -//! This directory contains symlinks to other files or directories in order to specify which node, -//! identity, credential or vault must be considered as a default when running a command expecting those -//! inputs -//! -//! # `identities` -//! -//! This directory contains one file per identity and a data directory. An identity file is created -//! with the `ockam identity create` command or created by default for some commands (in that case the -//! `defaults/identity` symlink points to that identity). The identity file contains: -//! -//! - the identity identifier -//! - the enrollment status for that identity -//! -//! The `data` directory contains a LMDB database with other information about identities: -//! - the credential attributes that have been verified for this identity. Those attributes are -//! generally used in ABAC rules that are specified on secure channels. For example when sending messages -//! via a secure channel and using the Orchestrator the `project` attribute will be checked and the LMDB database accessed -//! -//! - the list of key changes for each identity. These key changes are created (or updated) when an identity -//! is created either by using the command line or by using the identity service. -//! The key changes are accessed in order to get the latest public key associated to a given identity -//! when checking its signature during the creation of a secure channel. -//! They are also accessed to retrieve the key id associated to that key and then use a Vault to create a signature -//! for an identity -//! -//! Note: for each `.lmdb` file there is a corresponding `lmdb-lock` file which is used to control -//! the exclusive access to the LMDB database even if several OS processes are trying to modify it. -//! For example when several nodes are started using the same `NodeManager`. -//! -//! # `nodes` -//! -//! This directory contains: -//! -//! - symlinks to default values for the node: identity and vault -//! - a database for ABAC policies -//! - a setup file containing some configuration information for the node (is it an authority node?, what is the TCP listener address?,...). -//! That file is created when a node is created and read again if the node is restarted -//! - log files: for system errors and system outputs. The stdout.log file is where almost all the node logs are written -//! - a version number for the configuration -//! -//! # `projects` -//! -//! This directory contains a list of files, one per project that was created, either the default project -//! or via the `ockam project create` command. A project file contains: -//! -//! - the project identifier and the space it belongs to -//! - the authority used by that project (identity, route) -//! - the configuration for the project plugins -//! -//! # `trust_context` -//! -//! This directory contains a list of files, one per trust context. A trust context can created with -//! the `ockam trust_context create` command. It can then be referred to during the creation of a -//! secure channel as a way to specify which authority can attest to the validity of which attributes -//! -//! # `vaults` -//! -//! This directory contains one file per vault that is either created by default or with the `ockam vault create` -//! command. That file contains the configuration for the vault, which for now consists only in -//! declaring if the vault is backed by an AWS KMS or not. -//! -//! The rest of the vault data is stored in an LMDB database under the `data` directory with one `.lmdb` -//! file per vault. A vault contains secrets which are generally used during the creation of secure -//! channels to sign or encrypt data involved in the handshake. -//! + +#[macro_use] +extern crate tracing; + pub mod address; pub mod auth; pub mod authenticator; @@ -134,13 +30,11 @@ pub mod echoer; pub mod enroll; pub mod error; pub mod hop; -pub mod identity; pub mod kafka; pub mod minicbor_url; pub mod nodes; pub mod okta; pub mod port_range; -pub mod trust_context; pub mod uppercase; pub mod authority_node; @@ -151,181 +45,8 @@ mod schema; mod session; mod util; +pub use cli_state::*; pub use influxdb_token_lease::*; +pub use nodes::service::default_address::*; pub use session::sessions::ConnectionStatus; pub use util::*; - -#[macro_use] -extern crate tracing; - -pub struct DefaultAddress; - -impl DefaultAddress { - pub const AUTHENTICATED_SERVICE: &'static str = "authenticated"; - pub const RELAY_SERVICE: &'static str = "forwarding_service"; - pub const UPPERCASE_SERVICE: &'static str = "uppercase"; - pub const ECHO_SERVICE: &'static str = "echo"; - pub const HOP_SERVICE: &'static str = "hop"; - pub const CREDENTIALS_SERVICE: &'static str = "credentials"; - pub const SECURE_CHANNEL_LISTENER: &'static str = "api"; - pub const DIRECT_AUTHENTICATOR: &'static str = "direct_authenticator"; - pub const CREDENTIAL_ISSUER: &'static str = "credential_issuer"; - pub const ENROLLMENT_TOKEN_ISSUER: &'static str = "enrollment_token_issuer"; - pub const ENROLLMENT_TOKEN_ACCEPTOR: &'static str = "enrollment_token_acceptor"; - pub const OKTA_IDENTITY_PROVIDER: &'static str = "okta"; - pub const KAFKA_OUTLET: &'static str = "kafka_outlet"; - pub const KAFKA_CONSUMER: &'static str = "kafka_consumer"; - pub const KAFKA_PRODUCER: &'static str = "kafka_producer"; - pub const KAFKA_DIRECT: &'static str = "kafka_direct"; - - pub fn is_valid(name: &str) -> bool { - matches!( - name, - Self::AUTHENTICATED_SERVICE - | Self::RELAY_SERVICE - | Self::UPPERCASE_SERVICE - | Self::ECHO_SERVICE - | Self::HOP_SERVICE - | Self::CREDENTIALS_SERVICE - | Self::SECURE_CHANNEL_LISTENER - | Self::DIRECT_AUTHENTICATOR - | Self::CREDENTIAL_ISSUER - | Self::ENROLLMENT_TOKEN_ISSUER - | Self::ENROLLMENT_TOKEN_ACCEPTOR - | Self::OKTA_IDENTITY_PROVIDER - | Self::KAFKA_CONSUMER - | Self::KAFKA_PRODUCER - | Self::KAFKA_OUTLET - | Self::KAFKA_DIRECT - ) - } - - pub fn iter() -> impl Iterator { - [ - Self::AUTHENTICATED_SERVICE, - Self::RELAY_SERVICE, - Self::UPPERCASE_SERVICE, - Self::ECHO_SERVICE, - Self::HOP_SERVICE, - Self::CREDENTIALS_SERVICE, - Self::SECURE_CHANNEL_LISTENER, - Self::DIRECT_AUTHENTICATOR, - Self::CREDENTIAL_ISSUER, - Self::ENROLLMENT_TOKEN_ISSUER, - Self::ENROLLMENT_TOKEN_ACCEPTOR, - Self::OKTA_IDENTITY_PROVIDER, - Self::KAFKA_CONSUMER, - Self::KAFKA_PRODUCER, - Self::KAFKA_OUTLET, - Self::KAFKA_DIRECT, - ] - .iter() - .copied() - } -} - -pub mod actions { - use ockam_abac::Action; - - pub const HANDLE_MESSAGE: Action = Action::assert_inline("handle_message"); -} - -pub mod resources { - use ockam_abac::Resource; - - pub const INLET: Resource = Resource::assert_inline("tcp-inlet"); - pub const OUTLET: Resource = Resource::assert_inline("tcp-outlet"); -} - -use core::fmt; -use minicbor::{Decode, Encode}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -/// Newtype around [`Vec`] that provides base-16 string encoding using serde. -#[derive(Debug, Clone, Default, Encode, Decode)] -#[cbor(transparent)] -pub struct HexByteVec(#[n(0)] pub Vec); - -impl HexByteVec { - pub fn as_slice(&self) -> &[u8] { - &self.0 - } -} - -impl From> for HexByteVec { - fn from(v: Vec) -> Self { - Self(v) - } -} - -impl From for Vec { - fn from(h: HexByteVec) -> Self { - h.0 - } -} - -impl Serialize for HexByteVec { - fn serialize(&self, s: S) -> Result { - if s.is_human_readable() { - hex::serde::serialize(&*self.0, s) - } else { - s.serialize_bytes(&self.0) - } - } -} - -impl<'de> Deserialize<'de> for HexByteVec { - fn deserialize>(d: D) -> Result { - if d.is_human_readable() { - let v: Vec = hex::serde::deserialize(d)?; - Ok(Self(v)) - } else { - let v = >::deserialize(d)?; - Ok(Self(v)) - } - } -} - -impl fmt::Display for HexByteVec { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.serialize(f) - } -} - -#[cfg(test)] -mod test { - use super::DefaultAddress; - - #[test] - fn test_default_address_is_valid() { - assert!(!DefaultAddress::is_valid("foo")); - assert!(DefaultAddress::is_valid( - DefaultAddress::AUTHENTICATED_SERVICE - )); - assert!(DefaultAddress::is_valid(DefaultAddress::RELAY_SERVICE)); - assert!(DefaultAddress::is_valid(DefaultAddress::UPPERCASE_SERVICE)); - assert!(DefaultAddress::is_valid(DefaultAddress::ECHO_SERVICE)); - assert!(DefaultAddress::is_valid(DefaultAddress::HOP_SERVICE)); - assert!(DefaultAddress::is_valid( - DefaultAddress::CREDENTIALS_SERVICE - )); - assert!(DefaultAddress::is_valid( - DefaultAddress::SECURE_CHANNEL_LISTENER - )); - assert!(DefaultAddress::is_valid( - DefaultAddress::DIRECT_AUTHENTICATOR - )); - assert!(DefaultAddress::is_valid(DefaultAddress::CREDENTIAL_ISSUER)); - assert!(DefaultAddress::is_valid( - DefaultAddress::ENROLLMENT_TOKEN_ISSUER - )); - assert!(DefaultAddress::is_valid( - DefaultAddress::ENROLLMENT_TOKEN_ACCEPTOR - )); - assert!(DefaultAddress::is_valid( - DefaultAddress::OKTA_IDENTITY_PROVIDER - )); - assert!(DefaultAddress::is_valid(DefaultAddress::KAFKA_CONSUMER)); - assert!(DefaultAddress::is_valid(DefaultAddress::KAFKA_PRODUCER)); - } -} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/config.rs b/implementations/rust/ockam/ockam_api/src/nodes/config.rs deleted file mode 100644 index e8849194082..00000000000 --- a/implementations/rust/ockam/ockam_api/src/nodes/config.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::fmt::Formatter; -use std::io::Write; -use std::{fmt::Display, fs::File, io::Read, path::Path, str::FromStr}; - -use anyhow::anyhow; - -use crate::config::build_config_path; - -#[derive(Debug, Clone, PartialEq, Eq)] -enum NodeConfigVersion { - V0, - V1, -} - -#[allow(unused)] -impl NodeConfigVersion { - const FILE_NAME: &'static str = "version"; - - fn latest() -> Self { - Self::V1 - } - - fn load(config_dir: &Path) -> anyhow::Result { - let version_path = config_dir.join(Self::FILE_NAME); - let version = if version_path.exists() { - let mut version_file = File::open(version_path)?; - let mut version = String::new(); - version_file.read_to_string(&mut version)?; - NodeConfigVersion::from_str(&version)? - } else { - Self::V0 - }; - debug!(%version, "Loaded config"); - version.upgrade(config_dir) - } - - fn upgrade(&self, config_dir: &Path) -> anyhow::Result { - let from = self; - let mut final_version = from.clone(); - - // Iter through all the versions between `from` and `to` - let f = from.to_string().parse::()?; - let mut t = f + 1; - while let Ok(ref to) = Self::from_str(&t.to_string()) { - debug!(%from, %to, "Upgrading config"); - final_version = to.clone(); - #[allow(clippy::single_match)] - match (from, to) { - (Self::V0, Self::V1) => { - if let (Some(old_config_name), Some(new_config_name)) = - (from.state_config_name(), to.state_config_name()) - { - let old_config_path = build_config_path(config_dir, old_config_name); - // If old config path exists, copy to new config path and keep the old one - if old_config_path.exists() { - let new_config_path = build_config_path(config_dir, new_config_name); - std::fs::copy(old_config_path, new_config_path)?; - } - // Create the version file if doesn't exists - Self::set_version(config_dir, to)?; - } - } - _ => {} - } - t += 1; - } - Ok(final_version) - } - - fn dirs(&self) -> &'static [&'static str] { - match self { - Self::V0 => &["config"], - Self::V1 => &["state", "commands"], - } - } - - fn state_config_name(&self) -> Option<&'static str> { - match self { - Self::V0 => Some("config"), - Self::V1 => Some("state"), - } - } - - fn commands_config_name(&self) -> Option<&'static str> { - match self { - Self::V0 => None, - Self::V1 => Some("commands"), - } - } - - fn set_version(config_dir: &Path, version: &NodeConfigVersion) -> anyhow::Result<()> { - let version_path = config_dir.join(Self::FILE_NAME); - let mut version_file = File::create(version_path)?; - version_file.write_all(version.to_string().as_bytes())?; - Ok(()) - } -} - -impl Display for NodeConfigVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - NodeConfigVersion::V0 => "0", - NodeConfigVersion::V1 => "1", - }) - } -} - -impl FromStr for NodeConfigVersion { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "0" => Ok(Self::V0), - "1" => Ok(Self::V1), - _ => Err(anyhow!("Unknown version: {}", s)), - } - } -} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs index 5041499cb75..ef0c706c255 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/mod.rs @@ -12,8 +12,9 @@ use ockam_node::Context; use ockam_transport_tcp::{TcpConnection, TcpTransport}; use crate::error::ApiError; +use crate::multiaddr_to_route; +use crate::nodes::service::default_address::DefaultAddress; use crate::nodes::NodeManager; -use crate::{multiaddr_to_route, DefaultAddress}; pub(crate) use plain_tcp::PlainTcpInstantiator; pub(crate) use project::ProjectInstantiator; pub(crate) use secure::SecureChannelInstantiator; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/mod.rs b/implementations/rust/ockam/ockam_api/src/nodes/mod.rs index 5f925bf341c..2b4c7f45216 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/mod.rs @@ -1,14 +1,13 @@ -pub mod config; pub(crate) mod connection; pub mod models; pub mod registry; pub mod service; + pub use service::background_node::*; +pub use service::credentials::*; pub use service::in_memory_node::*; +/// The main node-manager service running on remote nodes +pub use service::{IdentityOverride, NodeManager, NodeManagerWorker}; /// A const address to bind and send messages to pub const NODEMANAGER_ADDR: &str = "_internal.nodemanager"; - -pub use service::credentials::*; -/// The main node-manager service running on remote nodes -pub use service::{IdentityOverride, NodeManager, NodeManagerWorker}; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/transport/json.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/transport/json.rs index 53c623078d5..2049d17a2da 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/transport/json.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/transport/json.rs @@ -37,3 +37,9 @@ impl CreateTransportJson { Ok(m) } } + +impl ToString for CreateTransportJson { + fn to_string(&self) -> String { + format!("{}/{}/{}", self.tt, self.tm, self.addr) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service.rs b/implementations/rust/ockam/ockam_api/src/nodes/service.rs index cfdc2587aa0..6723d555ac6 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service.rs @@ -1,41 +1,35 @@ //! Node Manager (Node Man, the superhero that we deserve) -use miette::IntoDiagnostic; use std::collections::BTreeMap; use std::error::Error as _; use std::net::SocketAddr; use std::path::PathBuf; use std::time::Duration; +use miette::IntoDiagnostic; use minicbor::{Decoder, Encode}; -pub use node_identities::*; use ockam::identity::models::CredentialAndPurposeKey; -use ockam::identity::CredentialsServerModule; use ockam::identity::TrustContext; -use ockam::identity::Vault; -use ockam::identity::{ - Credentials, CredentialsServer, Identities, IdentitiesRepository, IdentityAttributesReader, -}; +use ockam::identity::{Credentials, CredentialsServer, Identities}; +use ockam::identity::{CredentialsServerModule, IdentityAttributesRepository}; use ockam::identity::{Identifier, SecureChannels}; use ockam::{ Address, Context, RelayService, RelayServiceOptions, Result, Routed, TcpTransport, Worker, }; use ockam_abac::expr::{eq, ident, str}; -use ockam_abac::{Action, Env, Expr, PolicyAccessControl, PolicyStorage, Resource}; +use ockam_abac::{Action, Env, Expr, Resource}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::{string::String, sync::Arc}; use ockam_core::flow_control::FlowControlId; +use ockam_core::AllowAll; use ockam_core::IncomingAccessControl; -use ockam_core::{AllowAll, AsyncTryClone}; use ockam_multiaddr::MultiAddr; -use crate::bootstrapped_identities_store::BootstrapedIdentityStore; use crate::bootstrapped_identities_store::PreTrustedIdentities; -use crate::cli_state::{CliState, StateDirTrait, StateItemTrait}; +use crate::cli_state::CliState; +use crate::cli_state::NamedTrustContext; use crate::cloud::{AuthorityNode, ProjectNode}; -use crate::config::cli::TrustContextConfig; -use crate::config::lookup::ProjectLookup; use crate::error::ApiError; use crate::nodes::connection::{ Connection, ConnectionBuilder, PlainTcpInstantiator, ProjectInstantiator, @@ -46,22 +40,25 @@ use crate::nodes::models::portal::{OutletList, OutletStatus}; use crate::nodes::models::transport::{TransportMode, TransportType}; use crate::nodes::models::workers::{WorkerList, WorkerStatus}; use crate::nodes::registry::KafkaServiceKind; +use crate::nodes::service::default_address::DefaultAddress; use crate::nodes::{InMemoryNode, NODEMANAGER_ADDR}; use crate::session::MedicHandle; -use crate::DefaultAddress; use super::registry::Registry; +pub mod actions; pub(crate) mod background_node; pub(crate) mod credentials; +pub mod default_address; mod flow_controls; pub(crate) mod in_memory_node; pub mod message; -mod node_identities; mod node_services; mod policy; pub mod portals; +mod projects; pub mod relay; +pub mod resources; mod secure_channel; mod transport; @@ -94,20 +91,33 @@ pub(crate) fn encode_response>( pub struct NodeManager { pub(crate) cli_state: CliState, node_name: String, + node_identifier: Identifier, api_transport_flow_control_id: FlowControlId, pub(crate) tcp_transport: TcpTransport, - enable_credential_checks: bool, - identifier: Identifier, pub(crate) secure_channels: Arc, trust_context: Option, pub(crate) registry: Registry, - policies: Arc, pub(crate) medic_handle: MedicHandle, } impl NodeManager { - pub(super) fn identifier(&self) -> &Identifier { - &self.identifier + pub fn identifier(&self) -> Identifier { + self.node_identifier.clone() + } + + pub(crate) async fn get_identifier_by_name( + &self, + identity_name: Option, + ) -> Result { + if let Some(name) = identity_name { + Ok(self.cli_state.get_identifier_by_name(name.as_ref()).await?) + } else { + Ok(self.identifier()) + } + } + + pub fn trust_context_id(&self) -> Option { + self.trust_context.clone().map(|tc| tc.id().to_string()) } pub fn node_name(&self) -> String { @@ -118,12 +128,8 @@ impl NodeManager { self.secure_channels.identities() } - pub(super) fn identities_repository(&self) -> Arc { - self.identities().repository().clone() - } - - pub(super) fn attributes_reader(&self) -> Arc { - self.identities_repository().as_attributes_reader() + pub(super) fn identity_attributes_repository(&self) -> Arc { + self.identities().identity_attributes_repository().clone() } pub(super) fn credentials(&self) -> Arc { @@ -134,10 +140,6 @@ impl NodeManager { Arc::new(CredentialsServerModule::new(self.credentials())) } - pub(super) fn secure_channels_vault(&self) -> Vault { - self.secure_channels.identities().vault() - } - pub fn tcp_transport(&self) -> &TcpTransport { &self.tcp_transport } @@ -156,12 +158,10 @@ impl NodeManager { ) } - /// Delete the cli state related to the current node when launched in-memory - pub fn delete_node(&self) -> Result<()> { - Ok(self - .cli_state - .nodes - .delete_sigkill(self.node_name().as_str(), false)?) + /// Delete the current node data + pub async fn delete_node(&self) -> Result<()> { + self.cli_state.remove_node(&self.node_name).await?; + Ok(()) } } @@ -176,7 +176,7 @@ impl NodeManager { authority_identifier, authority_multiaddr, &self - .get_identifier(caller_identity_name) + .get_identifier_by_name(caller_identity_name) .await .into_diagnostic()?, ) @@ -194,7 +194,7 @@ impl NodeManager { project_identifier, project_multiaddr, &self - .get_identifier(caller_identity_name) + .get_identifier_by_name(caller_identity_name) .await .into_diagnostic()?, ) @@ -242,7 +242,7 @@ impl NodeManager { // Check if a policy exists for (resource, action) and if not, then // create or use a default entry: - if self.policies.get_policy(r, a).await?.is_none() { + if self.cli_state.get_policy(r, a).await?.is_none() { let fallback = match custom_default { Some(e) => e.clone(), None => eq([ @@ -250,17 +250,16 @@ impl NodeManager { ident("subject.trust_context_id"), ]), }; - self.policies.set_policy(r, a, &fallback).await? - } - let policies = self.policies.clone(); - Ok(Arc::new(PolicyAccessControl::new( - policies, - self.identities_repository(), - r.clone(), - a.clone(), - env, - ))) + self.cli_state.set_policy(r, a, &fallback).await?; + } + let policy_access_control = + self.cli_state.make_policy_access_control(r, a, env).await?; + Ok(Arc::new(policy_access_control)) } else { + debug!( + "no policy access control set for resource '{}' and action: '{}'", + &r, &a + ); Ok(Arc::new(AllowAll)) } } @@ -330,14 +329,12 @@ impl NodeManagerTransportOptions { } pub struct NodeManagerTrustOptions { - trust_context_config: Option, + trust_context: Option, } impl NodeManagerTrustOptions { - pub fn new(trust_context_config: Option) -> Self { - Self { - trust_context_config, - } + pub fn new(trust_context: Option) -> Self { + Self { trust_context } } } @@ -359,57 +356,46 @@ impl NodeManager { debug!("create the identity repository"); let cli_state = general_options.cli_state; - let node_state = cli_state.nodes.get(&general_options.node_name)?; - - let repository: Arc = - cli_state.identities.identities_repository().await?; - - //TODO: fix this. Either don't require it to be a bootstrappedidentitystore (and use the - //trait instead), or pass it from the general_options always. - let vault: Vault = node_state.config().vault().await?; - let identities_repository: Arc = - Arc::new(match general_options.pre_trusted_identities { - None => BootstrapedIdentityStore::new( - Arc::new(PreTrustedIdentities::new_from_string("{}")?), - repository.clone(), - ), - Some(f) => BootstrapedIdentityStore::new(Arc::new(f), repository.clone()), - }); - - debug!("create the secure channels service"); - let secure_channels = SecureChannels::builder() - .with_vault(vault) - .with_identities_repository(identities_repository.clone()) - .build(); - let policies: Arc = Arc::new(node_state.policies_storage().await?); + let secure_channels = cli_state + .secure_channels( + &general_options.node_name, + general_options.pre_trusted_identities, + ) + .await?; + + debug!("start the medic"); let medic_handle = MedicHandle::start_medic(ctx).await?; + debug!("create the trust context"); + let tcp_transport = transport_options.tcp_transport; + let trust_context = match trust_options.trust_context { + None => None, + Some(tc) => Some( + tc.trust_context(&tcp_transport, secure_channels.clone()) + .await?, + ), + }; + + debug!("retrieve the node identifier"); + let node_identifier = cli_state + .get_node(&general_options.node_name) + .await? + .identifier(); + let mut s = Self { cli_state, node_name: general_options.node_name, + node_identifier, api_transport_flow_control_id: transport_options.api_transport_flow_control_id, - tcp_transport: transport_options.tcp_transport, - enable_credential_checks: trust_options.trust_context_config.is_some() - && trust_options - .trust_context_config - .as_ref() - .unwrap() - .authority() - .is_ok(), - identifier: node_state.config().identifier()?, + tcp_transport, secure_channels, - trust_context: None, + trust_context, registry: Default::default(), - policies, medic_handle, }; - if let Some(tc) = trust_options.trust_context_config { - debug!("configuring trust context"); - s.configure_trust_context(&tc).await?; - } - + debug!("retrieve the node identifier"); s.initialize_services(ctx, general_options.start_default_services) .await?; info!("created a node manager for the node: {}", s.node_name); @@ -417,20 +403,6 @@ impl NodeManager { Ok(s) } - async fn configure_trust_context(&mut self, tc: &TrustContextConfig) -> Result<()> { - self.trust_context = Some( - tc.to_trust_context( - self.secure_channels.clone(), - Some(self.tcp_transport.async_try_clone().await?), - ) - .await?, - ); - - info!("NodeManager::configure_trust_context: trust context configured"); - - Ok(()) - } - async fn initialize_default_services( &self, ctx: &Context, @@ -500,15 +472,11 @@ impl NodeManager { &self, ctx: Arc, addr: &MultiAddr, - identifier: Option, + identifier: Identifier, authorized: Option, credential: Option, timeout: Option, ) -> Result { - let identifier = match identifier { - Some(identifier) => identifier, - None => self.get_identifier(None).await?, - }; let authorized = authorized.map(|authorized| vec![authorized]); self.connect(ctx, addr, identifier, authorized, credential, timeout) .await @@ -549,24 +517,8 @@ impl NodeManager { } pub(crate) async fn resolve_project(&self, name: &str) -> Result<(MultiAddr, Identifier)> { - let projects = ProjectLookup::from_state(self.cli_state.projects.list()?) - .await - .map_err(|e| ApiError::core(format!("Cannot load projects: {:?}", e)))?; - if let Some(info) = projects.get(name) { - let node_route = info - .node_route - .as_ref() - .ok_or_else(|| ApiError::core("Project should have node route set"))? - .clone(); - let identity_id = info - .identity_id - .as_ref() - .ok_or_else(|| ApiError::core("Project should have identity set"))? - .clone(); - Ok((node_route, identity_id)) - } else { - Err(ApiError::core(format!("project {name} not found"))) - } + let project = self.cli_state.get_project_by_name(name).await?; + Ok((project.access_route()?, project.identifier()?)) } } @@ -599,17 +551,14 @@ impl NodeManagerWorker { let r = match (method, path_segments.as_slice()) { // ==*== Basic node information ==*== // TODO: create, delete, destroy remote nodes - (Get, ["node"]) => { - let node_name = &self.node_manager.node_name(); - Response::ok(req) - .body(NodeStatus::new( - node_name, - "Running", - ctx.list_workers().await?.len() as u32, - std::process::id() as i32, - )) - .to_vec()? - } + (Get, ["node"]) => Response::ok(req) + .body(NodeStatus::new( + self.node_manager.node_name.clone(), + "Running", + ctx.list_workers().await?.len() as u32, + std::process::id() as i32, + )) + .to_vec()?, // ==*== Tcp Connection ==*== (Get, ["node", "tcp", "connection"]) => self.get_tcp_connections(req).await.to_vec()?, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/actions.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/actions.rs new file mode 100644 index 00000000000..90774f9c1bd --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/actions.rs @@ -0,0 +1,3 @@ +use ockam_abac::Action; + +pub const HANDLE_MESSAGE: Action = Action::assert_inline("handle_message"); diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs index 0cdcaacccb8..820a91c60c8 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/background_node.rs @@ -1,14 +1,17 @@ -use crate::cli_state::{CliState, StateDirTrait, StateItemTrait}; -use crate::nodes::NODEMANAGER_ADDR; +use std::sync::Arc; +use std::time::Duration; + use miette::IntoDiagnostic; use minicbor::{Decode, Encode}; + use ockam_core::api::{Reply, Request}; use ockam_core::{AsyncTryClone, Route}; use ockam_node::api::Client; use ockam_node::Context; use ockam_transport_tcp::{TcpConnectionOptions, TcpTransport}; -use std::sync::Arc; -use std::time::Duration; + +use crate::cli_state::CliState; +use crate::nodes::NODEMANAGER_ADDR; /// This struct represents a node that has been started /// on the same machine with a given node name @@ -28,7 +31,22 @@ impl BackgroundNode { /// Create a new client to send requests to a running background node /// This function instantiates a TcpTransport. Since a TcpTransport can only be created once /// this function must only be called once + /// + /// The optional node name is used to locate the node. It is either + /// a node specified by the user or the default node if no node name is given. pub async fn create( + ctx: &Context, + cli_state: &CliState, + node_name: &Option, + ) -> miette::Result { + let node_name = match node_name.clone() { + Some(name) => name, + None => cli_state.get_default_node().await?.name(), + }; + Self::create_to_node(ctx, cli_state, &node_name).await + } + + pub async fn create_to_node( ctx: &Context, cli_state: &CliState, node_name: &str, @@ -43,7 +61,6 @@ impl BackgroundNode { cli_state: &CliState, node_name: &str, ) -> miette::Result { - cli_state.nodes.get(node_name)?; Ok(BackgroundNode { cli_state: cli_state.clone(), node_name: node_name.to_string(), @@ -53,8 +70,8 @@ impl BackgroundNode { }) } - pub fn delete(&self) -> miette::Result<()> { - Ok(self.cli_state.nodes.delete(self.node_name())?) + pub async fn delete(&self) -> miette::Result<()> { + Ok(self.cli_state.delete_node(&self.node_name(), false).await?) } // Set a different node name @@ -63,6 +80,10 @@ impl BackgroundNode { self } + pub fn node_name(&self) -> String { + self.node_name.clone() + } + /// Use a default timeout for making requests pub fn set_timeout(&mut self, timeout: Duration) -> &Self { self.timeout = Some(timeout); @@ -73,10 +94,6 @@ impl BackgroundNode { &self.cli_state } - pub fn node_name(&self) -> &str { - &self.node_name - } - /// Send a request and expect a decodable response pub async fn ask(&self, ctx: &Context, req: Request) -> miette::Result where @@ -154,12 +171,20 @@ impl BackgroundNode { /// Make a route to the node and connect using TCP async fn create_route(&self) -> miette::Result { let mut route = self.to.clone(); - let node_state = self.cli_state.nodes.get(&self.node_name)?; - let port = node_state.config().setup().api_transport()?.addr.port(); - let addr_str = format!("localhost:{port}"); + let node_info = self.cli_state.get_node(&self.node_name).await?; + let tcp_listener_address = node_info + .tcp_listener_address() + .unwrap_or_else(|| { + panic!( + "an api transport should have been started for node {:?}", + &node_info + ) + }) + .to_string(); + let addr = self .tcp_transport - .connect(addr_str, TcpConnectionOptions::new()) + .connect(tcp_listener_address, TcpConnectionOptions::new()) .await .into_diagnostic()? .sender_address() diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/credentials.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/credentials.rs index 6d459e5bad2..fff4c3726d0 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/credentials.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/credentials.rs @@ -11,7 +11,6 @@ use ockam_core::async_trait; use ockam_multiaddr::MultiAddr; use ockam_node::Context; -use crate::cli_state::traits::StateDirTrait; use crate::cloud::AuthorityNode; use crate::error::ApiError; use crate::local_multiaddr_to_route; @@ -121,24 +120,21 @@ impl NodeManagerWorker { ) -> Result, Response>> { let request: GetCredentialRequest = dec.decode()?; - let identifier = if let Some(identity) = &request.identity_name { - self.node_manager - .cli_state - .identities - .get(identity)? - .identifier() - } else { - self.node_manager.identifier().clone() - }; + let identifier = self + .node_manager + .get_identifier_by_name(request.identity_name) + .await?; match self .node_manager - .trust_context()? - .authority()? - .credential(ctx, &identifier) + .get_credential(ctx, &identifier, None) .await { - Ok(c) => Ok(Either::Right(Response::ok(req).body(c))), + Ok(Some(c)) => Ok(Either::Right(Response::ok(req).body(c))), + Ok(None) => Ok(Either::Left(Response::not_found( + req, + &format!("no credential found for {}", identifier), + ))), Err(e) => Ok(Either::Left(Response::internal_error( req, &format!( @@ -166,12 +162,12 @@ impl NodeManagerWorker { })?; let route = local_multiaddr_to_route(&route)?; + let identifier = self.node_manager.identifier(); let credential = self .node_manager - .trust_context()? - .authority()? - .credential(ctx, self.node_manager.identifier()) - .await?; + .get_credential(ctx, &identifier, None) + .await? + .unwrap_or_else(|| panic!("A credential must be retrieved for {}", identifier)); if request.oneway { self.node_manager @@ -184,11 +180,7 @@ impl NodeManagerWorker { .present_credential_mutual( ctx, route, - self.node_manager - .trust_context()? - .authorities() - .await? - .as_slice(), + &self.node_manager.trust_context()?.authorities(), credential, ) .await?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs new file mode 100644 index 00000000000..6739ecd64b8 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/default_address.rs @@ -0,0 +1,103 @@ +pub struct DefaultAddress; + +impl DefaultAddress { + pub const AUTHENTICATED_SERVICE: &'static str = "authenticated"; + pub const RELAY_SERVICE: &'static str = "forwarding_service"; + pub const UPPERCASE_SERVICE: &'static str = "uppercase"; + pub const ECHO_SERVICE: &'static str = "echo"; + pub const HOP_SERVICE: &'static str = "hop"; + pub const CREDENTIALS_SERVICE: &'static str = "credentials"; + pub const SECURE_CHANNEL_LISTENER: &'static str = "api"; + pub const DIRECT_AUTHENTICATOR: &'static str = "direct_authenticator"; + pub const CREDENTIAL_ISSUER: &'static str = "credential_issuer"; + pub const ENROLLMENT_TOKEN_ISSUER: &'static str = "enrollment_token_issuer"; + pub const ENROLLMENT_TOKEN_ACCEPTOR: &'static str = "enrollment_token_acceptor"; + pub const OKTA_IDENTITY_PROVIDER: &'static str = "okta"; + pub const KAFKA_OUTLET: &'static str = "kafka_outlet"; + pub const KAFKA_CONSUMER: &'static str = "kafka_consumer"; + pub const KAFKA_PRODUCER: &'static str = "kafka_producer"; + pub const KAFKA_DIRECT: &'static str = "kafka_direct"; + + pub fn is_valid(name: &str) -> bool { + matches!( + name, + Self::AUTHENTICATED_SERVICE + | Self::RELAY_SERVICE + | Self::UPPERCASE_SERVICE + | Self::ECHO_SERVICE + | Self::HOP_SERVICE + | Self::CREDENTIALS_SERVICE + | Self::SECURE_CHANNEL_LISTENER + | Self::DIRECT_AUTHENTICATOR + | Self::CREDENTIAL_ISSUER + | Self::ENROLLMENT_TOKEN_ISSUER + | Self::ENROLLMENT_TOKEN_ACCEPTOR + | Self::OKTA_IDENTITY_PROVIDER + | Self::KAFKA_CONSUMER + | Self::KAFKA_PRODUCER + | Self::KAFKA_OUTLET + | Self::KAFKA_DIRECT + ) + } + + pub fn iter() -> impl Iterator { + [ + Self::AUTHENTICATED_SERVICE, + Self::RELAY_SERVICE, + Self::UPPERCASE_SERVICE, + Self::ECHO_SERVICE, + Self::HOP_SERVICE, + Self::CREDENTIALS_SERVICE, + Self::SECURE_CHANNEL_LISTENER, + Self::DIRECT_AUTHENTICATOR, + Self::CREDENTIAL_ISSUER, + Self::ENROLLMENT_TOKEN_ISSUER, + Self::ENROLLMENT_TOKEN_ACCEPTOR, + Self::OKTA_IDENTITY_PROVIDER, + Self::KAFKA_CONSUMER, + Self::KAFKA_PRODUCER, + Self::KAFKA_OUTLET, + Self::KAFKA_DIRECT, + ] + .iter() + .copied() + } +} + +#[cfg(test)] +mod test { + use super::DefaultAddress; + + #[test] + fn test_default_address_is_valid() { + assert!(!DefaultAddress::is_valid("foo")); + assert!(DefaultAddress::is_valid( + DefaultAddress::AUTHENTICATED_SERVICE + )); + assert!(DefaultAddress::is_valid(DefaultAddress::RELAY_SERVICE)); + assert!(DefaultAddress::is_valid(DefaultAddress::UPPERCASE_SERVICE)); + assert!(DefaultAddress::is_valid(DefaultAddress::ECHO_SERVICE)); + assert!(DefaultAddress::is_valid(DefaultAddress::HOP_SERVICE)); + assert!(DefaultAddress::is_valid( + DefaultAddress::CREDENTIALS_SERVICE + )); + assert!(DefaultAddress::is_valid( + DefaultAddress::SECURE_CHANNEL_LISTENER + )); + assert!(DefaultAddress::is_valid( + DefaultAddress::DIRECT_AUTHENTICATOR + )); + assert!(DefaultAddress::is_valid(DefaultAddress::CREDENTIAL_ISSUER)); + assert!(DefaultAddress::is_valid( + DefaultAddress::ENROLLMENT_TOKEN_ISSUER + )); + assert!(DefaultAddress::is_valid( + DefaultAddress::ENROLLMENT_TOKEN_ACCEPTOR + )); + assert!(DefaultAddress::is_valid( + DefaultAddress::OKTA_IDENTITY_PROVIDER + )); + assert!(DefaultAddress::is_valid(DefaultAddress::KAFKA_CONSUMER)); + assert!(DefaultAddress::is_valid(DefaultAddress::KAFKA_PRODUCER)); + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs index a75c8564453..c1407178cd3 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs @@ -1,22 +1,24 @@ -use miette::IntoDiagnostic; use std::ops::Deref; -use std::path::PathBuf; +use futures::executor; +use miette::IntoDiagnostic; + +use ockam::identity::SecureChannels; use ockam::{Context, Result, TcpTransport}; use ockam_core::compat::{string::String, sync::Arc}; use ockam_core::errcode::Kind; use ockam_transport_tcp::TcpListenerOptions; use crate::cli_state::random_name; -use crate::cli_state::{add_project_info_to_node_state, init_node_state, CliState}; +use crate::cli_state::CliState; +use crate::cli_state::NamedTrustContext; use crate::cloud::Controller; -use crate::config::cli::TrustContextConfig; +use crate::nodes::service::default_address::DefaultAddress; use crate::nodes::service::{ NodeManagerGeneralOptions, NodeManagerTransportOptions, NodeManagerTrustOptions, }; use crate::nodes::{NodeManager, NODEMANAGER_ADDR}; use crate::session::sessions::Session; -use crate::DefaultAddress; /// An `InMemoryNode` represents a full running node /// In addition to a `NodeManager`, which is used to handle all the entities related to a node @@ -51,9 +53,12 @@ impl Drop for InMemoryNode { // stops. Except if they have been started with the `ockam node create` command // because in that case they can be restarted if !self.persistent { - self.node_manager - .delete_node() - .unwrap_or_else(|_| panic!("cannot delete the node {}", self.node_name)); + executor::block_on(async { + self.node_manager + .delete_node() + .await + .unwrap_or_else(|e| panic!("cannot delete the node {}: {e:?}", self.node_name)) + }); } } } @@ -68,40 +73,34 @@ impl InMemoryNode { pub async fn start_with_trust_context( ctx: &Context, cli_state: &CliState, - project_path: Option<&PathBuf>, - trust_context_config: Option, + project_name: Option, + trust_context: Option, ) -> miette::Result { - Self::start_node( - ctx, - cli_state, - None, - None, - project_path, - trust_context_config, - ) - .await + Self::start_node(ctx, cli_state, None, None, project_name, trust_context).await } /// Start an in memory node pub async fn start_node( ctx: &Context, cli_state: &CliState, - vault: Option, - identity: Option, - project_path: Option<&PathBuf>, - trust_context_config: Option, + identity_name: Option, + vault_name: Option, + project_name: Option, + trust_context: Option, ) -> miette::Result { let defaults = NodeManagerDefaults::default(); - init_node_state( - cli_state, - &defaults.node_name, - vault.as_deref(), - identity.as_deref(), - ) - .await?; - - add_project_info_to_node_state(&defaults.node_name, cli_state, project_path).await?; + // if no identity is specified, create one + let identity = cli_state + .create_identity_with_optional_name_and_optional_vault(&identity_name, &vault_name) + .await?; + let node = cli_state + .create_node_with_optional_values( + &defaults.node_name, + &Some(identity.name()), + &project_name, + ) + .await?; let tcp = TcpTransport::create(ctx).await.into_diagnostic()?; let bind = defaults.tcp_listener_address; @@ -111,15 +110,9 @@ impl InMemoryNode { let node_manager = Self::new( ctx, - NodeManagerGeneralOptions::new( - cli_state.clone(), - defaults.node_name.clone(), - None, - false, - false, - ), + NodeManagerGeneralOptions::new(cli_state.clone(), node.name(), None, false, false), NodeManagerTransportOptions::new(listener.flow_control_id().clone(), tcp), - NodeManagerTrustOptions::new(trust_context_config), + NodeManagerTrustOptions::new(trust_context), ) .await .into_diagnostic()?; @@ -173,6 +166,10 @@ impl InMemoryNode { persistent, }) } + + pub fn secure_channels(&self) -> Arc { + self.secure_channels.clone() + } } pub struct NodeManagerDefaults { diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/message.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/message.rs index b58c64f4378..6b1116f0a48 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/message.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/message.rs @@ -76,7 +76,7 @@ impl MessageSender for NodeManager { let msg_length = message.len(); let connection_ctx = Arc::new(ctx.async_try_clone().await?); let connection = self - .make_connection(connection_ctx, addr, None, None, None, timeout) + .make_connection(connection_ctx, addr, self.identifier(), None, None, timeout) .await?; let route = connection.route(self.tcp_transport()).await?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/node_identities.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/node_identities.rs deleted file mode 100644 index 1a11e5383de..00000000000 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/node_identities.rs +++ /dev/null @@ -1,54 +0,0 @@ -use ockam::compat::sync::Arc; -use ockam::identity::{Identifier, Identities, Vault}; -use ockam::Result; - -use crate::cli_state::traits::StateDirTrait; -use crate::cli_state::CliState; - -/// This struct supports identities operation that are either backed by -/// a specific vault or which are using the default vault -pub struct NodeIdentities { - identities: Arc, - cli_state: CliState, -} - -impl NodeIdentities { - pub fn new(identities: Arc, cli_state: CliState) -> NodeIdentities { - NodeIdentities { - identities, - cli_state, - } - } - - pub(super) fn identities_vault(&self) -> Vault { - self.identities.vault() - } - - pub(crate) async fn get_identifier(&self, identity_name: String) -> Result { - let identity_state = self.cli_state.identities.get(identity_name.as_str())?; - Ok(identity_state.identifier()) - } - - /// Return an identities service, possibly backed by a specific vault - pub(crate) async fn get_identities( - &self, - vault_name: Option, - ) -> Result> { - let vault = self.get_identities_vault(vault_name).await?; - let repository = self.cli_state.identities.identities_repository().await?; - Ok(Identities::builder() - .with_vault(vault) - .with_identities_repository(repository) - .build()) - } - - /// Return either the default vault or a specific one - pub(crate) async fn get_identities_vault(&self, vault_name: Option) -> Result { - if let Some(vault) = vault_name { - let existing_vault = self.cli_state.vaults.get(vault.as_str())?.get().await?; - Ok(existing_vault) - } else { - Ok(self.identities_vault()) - } - } -} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs index 859be671f61..110f509e934 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs @@ -30,16 +30,15 @@ use crate::nodes::models::services::{ use crate::nodes::registry::{ CredentialsServiceInfo, KafkaServiceInfo, KafkaServiceKind, Registry, }; +use crate::nodes::service::default_address::DefaultAddress; use crate::nodes::NodeManager; use crate::port_range::PortRange; use crate::uppercase::Uppercase; -use crate::DefaultAddress; -use crate::{actions, resources}; -use super::NodeManagerWorker; +use super::{actions, resources, NodeManagerWorker}; impl NodeManager { - pub(super) async fn start_credentials_service_impl<'a>( + pub(super) async fn start_credentials_service_impl( &self, ctx: &Context, trust_context: TrustContext, @@ -51,13 +50,7 @@ impl NodeManager { } self.credentials_service() - .start( - ctx, - trust_context, - self.identifier().clone(), - addr.clone(), - !oneway, - ) + .start(ctx, trust_context, self.identifier(), addr.clone(), !oneway) .await?; self.registry @@ -84,7 +77,7 @@ impl NodeManager { )); } - let server = Server::new(self.attributes_reader()); + let server = Server::new(self.identity_attributes_repository()); ctx.start_worker(addr.clone(), server).await?; self.registry @@ -236,6 +229,7 @@ impl NodeManagerWorker { let decoded_identity = &hex::decode(encoded_identity).map_err(|_| ApiError::core("Unable to decode trust context's public identity when starting credential service."))?; let i = identities() + .await? .identities_creation() .import(None, decoded_identity) .await?; @@ -538,13 +532,14 @@ impl NodeManagerWorker { // if we are using the project we need to allow safe communication based on the // project identifier self.node_manager - .policies + .cli_state .set_policy( &resources::INLET, &actions::HANDLE_MESSAGE, &eq([ident("subject.identifier"), str(project_identifier)]), ) - .await?; + .await + .map_err(ockam_core::Error::from)? } } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/policy.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/policy.rs index 5c375e151c7..c0140f35733 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/policy.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/policy.rs @@ -1,8 +1,6 @@ use either::Either; use minicbor::Decoder; -use crate::cli_state::{StateDirTrait, StateItemTrait}; -use crate::nodes::BackgroundNode; use ockam_abac::expr::{eq, ident, str}; use ockam_abac::{Action, Resource}; use ockam_core::api::{Error, Request, RequestHeader, Response}; @@ -10,6 +8,7 @@ use ockam_core::{async_trait, Result}; use ockam_node::Context; use crate::nodes::models::policy::{Expression, Policy, PolicyList}; +use crate::nodes::BackgroundNode; use super::NodeManager; @@ -24,7 +23,10 @@ impl NodeManager { let p: Policy = dec.decode()?; let r = Resource::new(resource); let a = Action::new(action); - self.policies.set_policy(&r, &a, p.expression()).await?; + self.cli_state + .set_policy(&r, &a, p.expression()) + .await + .map_err(ockam_core::Error::from)?; Ok(Response::ok(req)) } @@ -36,7 +38,7 @@ impl NodeManager { ) -> Result, Response>> { let r = Resource::new(resource); let a = Action::new(action); - if let Some(e) = self.policies.get_policy(&r, &a).await? { + if let Some(e) = self.cli_state.get_policy(&r, &a).await? { Ok(Either::Right(Response::ok(req).body(Policy::new(e)))) } else { Ok(Either::Left(Response::not_found(req, "policy not found"))) @@ -49,7 +51,11 @@ impl NodeManager { res: &str, ) -> Result, Response> { let r = Resource::new(res); - let p = self.policies.policies(&r).await?; + let p = self + .cli_state + .get_policies_by_resource(&r) + .await + .map_err(ockam_core::Error::from)?; let p = p.into_iter().map(|(a, e)| Expression::new(a, e)).collect(); Ok(Response::ok(req).body(PolicyList::new(p))) } @@ -62,7 +68,10 @@ impl NodeManager { ) -> Result, Response> { let r = Resource::new(res); let a = Action::new(act); - self.policies.del_policy(&r, &a).await?; + self.cli_state + .delete_policy(&r, &a) + .await + .map_err(ockam_core::Error::from)?; Ok(Response::ok(req)) } } @@ -86,12 +95,9 @@ impl Policies for BackgroundNode { ) -> miette::Result<()> { let project_id = match self .cli_state() - .nodes - .get(self.node_name())? - .config() - .setup() - .project - .to_owned() + .get_node_project(&self.node_name()) + .await + .ok() { None => return Ok(()), Some(p) => p.id, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs index 2de7a17bc63..8abb0d5bd0f 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs @@ -1,7 +1,8 @@ -use minicbor::Decoder; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use std::time::Duration; + +use minicbor::Decoder; use tokio::time::timeout; use ockam::identity::Identifier; @@ -15,21 +16,19 @@ use ockam_multiaddr::{MultiAddr, Protocol}; use ockam_node::Context; use ockam_transport_tcp::{TcpInletOptions, TcpOutletOptions}; -use crate::cli_state::StateDirTrait; -use crate::config::lookup::ProjectLookup; use crate::error::ApiError; use crate::nodes::connection::Connection; use crate::nodes::models::portal::{ CreateInlet, CreateOutlet, InletList, InletStatus, OutletList, OutletStatus, }; use crate::nodes::registry::{InletInfo, OutletInfo}; +use crate::nodes::service::default_address::DefaultAddress; use crate::nodes::service::policy::Policies; -use crate::nodes::service::random_alias; +use crate::nodes::service::{actions, random_alias, resources}; use crate::nodes::{BackgroundNode, InMemoryNode}; use crate::session::sessions::{ ConnectionStatus, Replacer, Session, MAX_CONNECT_TIME, MAX_RECOVERY_TIME, }; -use crate::{actions, resources, DefaultAddress}; use super::{NodeManager, NodeManagerWorker}; @@ -185,8 +184,8 @@ impl NodeManager { reachable_from_default_secure_channel: bool, ) -> Result { info!( - "Handling request to create outlet portal at {:?}", - socket_addr + "Handling request to create outlet portal at {:?} with worker {:?}", + socket_addr, worker_addr ); let resource = alias .as_deref() @@ -205,19 +204,17 @@ impl NodeManager { )); } - let check_credential = self.enable_credential_checks; - let trust_context_id = if check_credential { - Some(self.trust_context()?.id()) - } else { - None - }; - let access_control = self - .access_control(&resource, &actions::HANDLE_MESSAGE, trust_context_id, None) + .access_control( + &resource, + &actions::HANDLE_MESSAGE, + self.trust_context_id().as_deref(), + None, + ) .await?; let options = TcpOutletOptions::new().with_incoming_access_control(access_control); - let options = if !check_credential { + let options = if self.trust_context_id().is_none() { options.as_consumer(&self.api_transport_flow_control_id) } else { options @@ -359,36 +356,39 @@ impl NodeManager { let outlet_route = connection.route(self.tcp_transport()).await?; let outlet_route = route![prefix_route.clone(), outlet_route, suffix_route.clone()]; - let projects = self.cli_state.projects.list()?; - let projects = ProjectLookup::from_state(projects) - .await - .map_err(|e| ockam_core::Error::new(Origin::Node, Kind::NotFound, e))?; - let check_credential = self.enable_credential_checks; - let project_id = if check_credential { - let pid = outlet_addr - .first() - .and_then(|p| { - if let Some(p) = p.cast::() { - projects.get(&*p).map(|info| &*info.id) - } else { - None - } - }) - .or_else(|| Some(self.trust_context().ok()?.id())); - if pid.is_none() { - let message = "Credential check requires a project or trust context"; - return Err(ockam_core::Error::new(Origin::Node, Kind::Invalid, message)); + let projects = self.cli_state.get_projects_grouped_by_name().await?; + + let project_id = match self.trust_context_id() { + Some(trust_context_id) => { + let pid = outlet_addr + .first() + .and_then(|p| { + if let Some(p) = p.cast::() { + projects.get(&*p).map(|project| project.id()) + } else { + None + } + }) + .or(Some(trust_context_id)); + if pid.is_none() { + let message = "Credential check requires a project or trust context"; + return Err(ockam_core::Error::new(Origin::Node, Kind::Invalid, message)); + } + pid } - pid - } else { - None + None => None, }; let resource = requested_alias .map(|a| Resource::new(a.as_str())) .unwrap_or(resources::INLET); let access_control = self - .access_control(&resource, &actions::HANDLE_MESSAGE, project_id, None) + .access_control( + &resource, + &actions::HANDLE_MESSAGE, + project_id.as_deref(), + None, + ) .await?; let options = TcpInletOptions::new().with_incoming_access_control(access_control.clone()); @@ -550,7 +550,7 @@ impl InMemoryNode { .make_connection( connection_ctx.clone(), &outlet_addr, - None, + self.identifier(), authorized.clone(), None, Some(duration), @@ -670,7 +670,7 @@ impl InMemoryNode { .make_connection( ctx.clone(), &addr, - None, + node_manager.identifier(), authorized, None, Some(MAX_CONNECT_TIME), diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/projects.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/projects.rs new file mode 100644 index 00000000000..9730c1ef7d1 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/projects.rs @@ -0,0 +1,107 @@ +use ockam_core::async_trait; +use ockam_node::Context; + +use crate::cloud::project::{OrchestratorVersionInfo, Project, Projects}; +use crate::nodes::InMemoryNode; + +#[async_trait] +impl Projects for InMemoryNode { + async fn create_project( + &self, + ctx: &Context, + space_name: &str, + project_name: &str, + users: Vec, + ) -> miette::Result { + let space = self.cli_state.get_space_by_name(space_name).await?; + let controller = self.create_controller().await?; + let project = controller + .create_project(ctx, &space.space_id(), project_name, users) + .await?; + self.cli_state.store_project(project.clone()).await?; + Ok(project) + } + + async fn get_project(&self, ctx: &Context, project_id: &str) -> miette::Result { + let controller = self.create_controller().await?; + let project = controller.get_project(ctx, project_id).await?; + self.cli_state.store_project(project.clone()).await?; + Ok(project) + } + + async fn get_project_by_name_or_default( + &self, + ctx: &Context, + project_name: &Option, + ) -> miette::Result { + let project_id = self + .cli_state + .get_project_by_name_or_default(project_name) + .await? + .id(); + self.get_project(ctx, &project_id).await + } + + async fn get_project_by_name( + &self, + ctx: &Context, + project_name: &str, + ) -> miette::Result { + let project_id = self.cli_state.get_project_by_name(project_name).await?.id(); + self.get_project(ctx, &project_id).await + } + + async fn delete_project( + &self, + ctx: &Context, + space_id: &str, + project_id: &str, + ) -> miette::Result<()> { + let controller = self.create_controller().await?; + controller.delete_project(ctx, space_id, project_id).await?; + Ok(self.cli_state.delete_project(project_id).await?) + } + + async fn delete_project_by_name( + &self, + ctx: &Context, + space_name: &str, + project_name: &str, + ) -> miette::Result<()> { + let space = self.cli_state.get_space_by_name(space_name).await?; + let project = self.cli_state.get_project_by_name(project_name).await?; + self.delete_project(ctx, &space.space_id(), &project.id()) + .await + } + + async fn get_orchestrator_version_info( + &self, + ctx: &Context, + ) -> miette::Result { + Ok(self + .create_controller() + .await? + .get_orchestrator_version_info(ctx) + .await?) + } + + async fn get_projects(&self, ctx: &Context) -> miette::Result> { + let projects = self.create_controller().await?.list_projects(ctx).await?; + for project in &projects { + self.cli_state.store_project(project.clone()).await? + } + Ok(projects) + } + + async fn wait_until_project_is_ready( + &self, + ctx: &Context, + project: Project, + ) -> miette::Result { + Ok(self + .create_controller() + .await? + .wait_until_project_is_ready(ctx, project) + .await?) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs index 82d7f888548..0cdaa090235 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs @@ -1,7 +1,8 @@ -use miette::IntoDiagnostic; use std::sync::Arc; use std::time::Duration; +use miette::IntoDiagnostic; + use ockam::compat::sync::Mutex; use ockam::identity::Identifier; use ockam::remote::{RemoteRelay, RemoteRelayOptions}; @@ -233,7 +234,7 @@ impl InMemoryNode { .make_connection( connection_ctx.clone(), &address.clone(), - None, + self.identifier(), authorized.clone(), None, None, @@ -337,7 +338,7 @@ impl InMemoryNode { .make_connection( ctx.clone(), &addr, - None, + node_manager.identifier(), authorized, None, Some(MAX_CONNECT_TIME), diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/resources.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/resources.rs new file mode 100644 index 00000000000..305353aeb58 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/resources.rs @@ -0,0 +1,4 @@ +use ockam_abac::Resource; + +pub const INLET: Resource = Resource::assert_inline("tcp-inlet"); +pub const OUTLET: Resource = Resource::assert_inline("tcp-outlet"); diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs index 38ff5f15b94..7ee9cef75f6 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs @@ -7,7 +7,7 @@ use ockam::identity::models::CredentialAndPurposeKey; use ockam::identity::TrustEveryonePolicy; use ockam::identity::Vault; use ockam::identity::{ - Identifier, Identities, SecureChannelListenerOptions, SecureChannelOptions, SecureChannels, + Identifier, SecureChannelListenerOptions, SecureChannelOptions, SecureChannels, TrustMultiIdentifiersPolicy, }; use ockam::identity::{SecureChannel, SecureChannelListener}; @@ -19,8 +19,6 @@ use ockam_core::AsyncTryClone; use ockam_multiaddr::MultiAddr; use ockam_node::Context; -use crate::cli_state::traits::StateDirTrait; -use crate::cli_state::StateItemTrait; use crate::nodes::models::secure_channel::{ CreateSecureChannelListenerRequest, CreateSecureChannelRequest, CreateSecureChannelResponse, DeleteSecureChannelListenerRequest, DeleteSecureChannelListenerResponse, @@ -29,9 +27,8 @@ use crate::nodes::models::secure_channel::{ ShowSecureChannelResponse, }; use crate::nodes::registry::{SecureChannelInfo, SecureChannelListenerInfo}; -use crate::nodes::service::NodeIdentities; +use crate::nodes::service::default_address::DefaultAddress; use crate::nodes::{NodeManager, NodeManagerWorker}; -use crate::DefaultAddress; /// SECURE CHANNELS impl NodeManagerWorker { @@ -80,7 +77,7 @@ impl NodeManagerWorker { return Err(Response::bad_request( req, &format!("Incorrect multi-address {}", addr), - )) + )); } }; let sc = self @@ -266,9 +263,9 @@ impl NodeManager { credential_name: Option, timeout: Option, ) -> Result { - let identifier = self.get_identifier(identity_name.clone()).await?; + let identifier = self.get_identifier_by_name(identity_name.clone()).await?; let credential = self - .get_credential(ctx, &identifier, credential_name, timeout) + .retrieve_credential(ctx, &identifier, credential_name, timeout) .await?; let connection_ctx = Arc::new(ctx.async_try_clone().await?); @@ -276,7 +273,7 @@ impl NodeManager { .make_connection( connection_ctx, &addr, - Some(identifier.clone()), + identifier.clone(), None, credential.clone(), timeout, @@ -297,7 +294,7 @@ impl NodeManager { Ok(sc) } - pub async fn get_credential( + pub async fn retrieve_credential( &self, ctx: &Context, identifier: &Identifier, @@ -305,35 +302,42 @@ impl NodeManager { timeout: Option, ) -> Result> { debug!("getting a credential"); - let credential = if let Some(credential_name) = credential_name { + if let Some(credential_name) = credential_name { debug!( "get the credential using a credential name {}", &credential_name ); - Some( + Ok(Some( self.cli_state - .credentials - .get(credential_name)? - .config() - .credential()?, - ) + .get_credential_by_name(&credential_name) + .await? + .credential_and_purpose_key(), + )) } else { - match self.trust_context().ok() { - Some(tc) => { - if let Some(t) = timeout { - ockam_node::compat::timeout(t, tc.get_credential(ctx, identifier)) - .await - .map_err(|e| { - ockam_core::Error::new(Origin::Api, Kind::Timeout, e.to_string()) - })? - } else { - tc.get_credential(ctx, identifier).await - } - } - None => None, + self.get_credential(ctx, identifier, timeout).await + } + } + + pub async fn get_credential( + &self, + ctx: &Context, + identifier: &Identifier, + timeout: Option, + ) -> Result> { + if let Some(tc) = self.trust_context.as_ref() { + debug!("getting a credential"); + if let Some(t) = timeout { + ockam_node::compat::timeout(t, tc.get_credential(ctx, identifier)) + .await + .map_err(|e| { + ockam_core::Error::new(Origin::Api, Kind::Timeout, e.to_string()) + })? + } else { + tc.get_credential(ctx, identifier).await } - }; - Ok(credential) + } else { + Ok(None) + } } pub(crate) async fn create_secure_channel_internal( @@ -356,8 +360,7 @@ impl NodeManager { let options = if let Some(credential) = credential { options.with_credential(credential) - } else if let Some(credential) = self.get_credential(ctx, identifier, None, timeout).await? - { + } else if let Some(credential) = self.get_credential(ctx, identifier, timeout).await? { options.with_credential(credential) } else { options @@ -422,7 +425,7 @@ impl NodeManager { ); let secure_channels = self.build_secure_channels(vault_name.clone()).await?; - let identifier = self.get_identifier(identity_name.clone()).await?; + let identifier = self.get_identifier_by_name(identity_name.clone()).await?; let options = SecureChannelListenerOptions::new().as_consumer(&self.api_transport_flow_control_id); @@ -503,37 +506,33 @@ impl NodeManager { return Ok(self.secure_channels.clone()); } let vault = self.get_secure_channels_vault(vault_name.clone()).await?; - let identities = self.get_identities(vault_name).await?; let registry = self.secure_channels.secure_channel_registry(); Ok(SecureChannels::builder() + .await? .with_vault(vault) - .with_identities(identities) + .with_change_history_repository( + self.secure_channels + .identities() + .change_history_repository(), + ) + .with_purpose_keys_repository( + self.secure_channels.identities().purpose_keys_repository(), + ) .with_secure_channels_registry(registry) .build()) } - pub fn node_identities(&self) -> NodeIdentities { - NodeIdentities::new(self.identities(), self.cli_state.clone()) - } - - pub async fn get_identifier(&self, identity_name: Option) -> Result { - if let Some(name) = identity_name { - self.node_identities().get_identifier(name.clone()).await - } else { - Ok(self.identifier().clone()) - } - } - - async fn get_identities(&self, vault_name: Option) -> Result> { - self.node_identities().get_identities(vault_name).await - } - async fn get_secure_channels_vault(&self, vault_name: Option) -> Result { - if let Some(vault) = vault_name { - let existing_vault = self.cli_state.vaults.get(vault.as_str())?.get().await?; + if let Some(vault_name) = vault_name { + let existing_vault = self + .cli_state + .get_named_vault(&vault_name) + .await? + .vault() + .await?; Ok(existing_vault) } else { - Ok(self.secure_channels_vault()) + Ok(self.secure_channels.vault()) } } } diff --git a/implementations/rust/ockam/ockam_api/src/okta/mod.rs b/implementations/rust/ockam/ockam_api/src/okta/mod.rs index 753b9398295..bd99c8fcfc8 100644 --- a/implementations/rust/ockam/ockam_api/src/okta/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/okta/mod.rs @@ -2,10 +2,8 @@ use crate::error::ApiError; use core::str; use minicbor::Decoder; use ockam::identity::utils::now; -use ockam::identity::TRUST_CONTEXT_ID; -use ockam::identity::{ - AttributesEntry, Identifier, IdentityAttributesWriter, IdentitySecureChannelLocalInfo, -}; +use ockam::identity::{AttributesEntry, Identifier, IdentitySecureChannelLocalInfo}; +use ockam::identity::{IdentityAttributesRepository, TRUST_CONTEXT_ID}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::sync::Arc; use ockam_core::{self, Result, Routed, Worker}; @@ -15,7 +13,7 @@ use std::collections::HashMap; use tracing::trace; pub struct Server { - attributes_writer: Arc, + identity_attributes_repository: Arc, project: String, tenant_base_url: String, certificate: reqwest::Certificate, @@ -42,7 +40,7 @@ impl Worker for Server { impl Server { pub fn new( - attributes_writer: Arc, + identity_attributes_repository: Arc, project: String, tenant_base_url: &str, certificate: &str, @@ -51,7 +49,7 @@ impl Server { let certificate = reqwest::Certificate::from_pem(certificate.as_bytes()) .map_err(|err| ApiError::core(err.to_string()))?; Ok(Server { - attributes_writer, + identity_attributes_repository, project, tenant_base_url: tenant_base_url.to_string(), certificate, @@ -102,7 +100,9 @@ impl Server { None, None, ); - self.attributes_writer.put_attributes(from, entry).await?; + self.identity_attributes_repository + .put_attributes(from, entry) + .await?; Response::ok(&req).to_vec()? } else { Response::forbidden(&req, "Forbidden").to_vec()? diff --git a/implementations/rust/ockam/ockam_api/src/session/mod.rs b/implementations/rust/ockam/ockam_api/src/session/mod.rs index 806f269f19a..aee4cd71175 100644 --- a/implementations/rust/ockam/ockam_api/src/session/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/session/mod.rs @@ -13,8 +13,8 @@ use ockam_node::tokio::time::{sleep, timeout, Duration}; use ockam_node::Context; use ockam_node::{tokio, WorkerBuilder}; +use crate::nodes::service::default_address::DefaultAddress; use crate::session::sessions::{ConnectionStatus, Ping, Session}; -use crate::DefaultAddress; pub(crate) mod sessions; diff --git a/implementations/rust/ockam/ockam_api/src/trust_context.rs b/implementations/rust/ockam/ockam_api/src/trust_context.rs deleted file mode 100644 index 6cb05deaabf..00000000000 --- a/implementations/rust/ockam/ockam_api/src/trust_context.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::cli_state::{CliState, ProjectConfigCompact, StateDirTrait, StateItemTrait}; -use crate::cloud::project::Project; -use crate::config::cli::TrustContextConfig; -use miette::{IntoDiagnostic, WrapErr}; -use std::path::PathBuf; - -#[derive(Debug, Clone)] -pub struct TrustContextConfigBuilder { - pub cli_state: CliState, - pub project_path: Option, - pub trust_context: Option, - pub project: Option, - pub authority_identity: Option, - pub credential_name: Option, - pub use_default_trust_context: bool, -} - -impl TrustContextConfigBuilder { - pub fn new(cli_state: &CliState) -> Self { - Self { - cli_state: cli_state.clone(), - project_path: None, - trust_context: None, - project: None, - authority_identity: None, - credential_name: None, - use_default_trust_context: false, - } - } - - pub fn with_authority_identity(&mut self, authority_identity: Option<&String>) -> &mut Self { - self.authority_identity = authority_identity.map(|s| s.to_string()); - self - } - - pub fn with_credential_name(&mut self, credential_name: Option<&String>) -> &mut Self { - self.credential_name = credential_name.map(|s| s.to_string()); - self - } - - pub fn use_default_trust_context(&mut self, use_default_trust_context: bool) -> &mut Self { - self.use_default_trust_context = use_default_trust_context; - self - } - - pub fn build(&self) -> Option { - self.trust_context - .clone() - .or_else(|| self.get_from_project_path(self.project_path.as_ref()?)) - .or_else(|| self.get_from_project_name()) - .or_else(|| self.get_from_authority_identity()) - .or_else(|| self.get_from_credential()) - .or_else(|| self.get_from_default_trust_context()) - .or_else(|| self.get_from_default_project()) - } - - fn get_from_project_path(&self, path: &PathBuf) -> Option { - let s = std::fs::read_to_string(path) - .into_diagnostic() - .context("Failed to read project file") - .ok()?; - let proj_info = serde_json::from_str::(&s) - .into_diagnostic() - .context("Failed to parse project info") - .ok()?; - let proj: Project = (&proj_info).into(); - proj.try_into().ok() - } - - fn get_from_project_name(&self) -> Option { - let project = self.cli_state.projects.get(self.project.as_ref()?).ok()?; - project.config().clone().try_into().ok() - } - - fn get_from_authority_identity(&self) -> Option { - let authority_identity = self.authority_identity.clone(); - let credential = match &self.credential_name { - Some(c) => Some(self.cli_state.credentials.get(c).ok()?), - None => None, - }; - - TrustContextConfig::from_authority_identity(&authority_identity?, credential).ok() - } - - fn get_from_credential(&self) -> Option { - let cred_name = self.credential_name.clone()?; - let cred_state = self.cli_state.credentials.get(cred_name).ok()?; - - cred_state.try_into().ok() - } - - fn get_from_default_trust_context(&self) -> Option { - if !self.use_default_trust_context { - return None; - } - - let tc = self - .cli_state - .trust_contexts - .default() - .ok()? - .config() - .clone(); - Some(tc) - } - - fn get_from_default_project(&self) -> Option { - let proj = self.cli_state.projects.default().ok()?; - self.get_from_project_path(proj.path()) - } -} diff --git a/implementations/rust/ockam/ockam_api/src/util.rs b/implementations/rust/ockam/ockam_api/src/util.rs index 1b94529e370..15bbacf7336 100644 --- a/implementations/rust/ockam/ockam_api/src/util.rs +++ b/implementations/rust/ockam/ockam_api/src/util.rs @@ -1,6 +1,7 @@ -use miette::miette; use std::net::{SocketAddrV4, SocketAddrV6}; +use miette::miette; + use ockam::TcpTransport; use ockam_core::errcode::{Kind, Origin}; use ockam_core::flow_control::FlowControlId; @@ -49,7 +50,7 @@ pub fn local_multiaddr_to_route(ma: &MultiAddr) -> Result { Origin::Api, Kind::Invalid, "unexpected code: node. clean_multiaddr should have been called", - )) + )); } code @ (Ip4::CODE | Ip6::CODE | DnsAddr::CODE) => { @@ -57,7 +58,7 @@ pub fn local_multiaddr_to_route(ma: &MultiAddr) -> Result { Origin::Api, Kind::Invalid, format!("unexpected code: {code}. The address must be a local address {ma}"), - )) + )); } other => { @@ -373,22 +374,17 @@ pub fn local_worker(code: &Code) -> Result { #[cfg(test)] pub mod test_utils { - use ockam::identity::storage::InMemoryStorage; use ockam::identity::utils::AttributesBuilder; - use ockam::identity::{Identifier, MAX_CREDENTIAL_VALIDITY}; + use ockam::identity::MAX_CREDENTIAL_VALIDITY; use ockam::identity::{SecureChannels, PROJECT_MEMBER_SCHEMA, TRUST_CONTEXT_ID}; use ockam::Result; use ockam_core::compat::sync::Arc; use ockam_core::flow_control::FlowControls; use ockam_core::AsyncTryClone; - use ockam_node::Context; use ockam_transport_tcp::TcpTransport; - use crate::cli_state::{ - random_name, traits::*, CliState, IdentityConfig, NodeConfig, VaultConfig, - }; - use crate::config::cli::{CredentialRetrieverConfig, TrustAuthorityConfig, TrustContextConfig}; + use crate::cli_state::{random_name, CliState}; use crate::nodes::service::{ NodeManagerGeneralOptions, NodeManagerTransportOptions, NodeManagerTrustOptions, }; @@ -405,12 +401,11 @@ pub mod test_utils { pub node_manager: Arc, pub tcp: TcpTransport, pub secure_channels: Arc, - pub identifier: Identifier, } impl Drop for NodeManagerHandle { fn drop(&mut self) { - CliState::delete_at(&self.cli_state.dir).expect("cannot delete cli state"); + self.cli_state.delete().expect("cannot delete cli state"); } } @@ -421,42 +416,23 @@ pub mod test_utils { // #[must_use] make sense to enable only on rust 1.67+ pub async fn start_manager_for_tests(context: &mut Context) -> Result { let tcp = TcpTransport::create(context).await?; - let cli_state = CliState::test()?; - - let vault_name = random_name(); - let vault = cli_state - .vaults - .create_async(&vault_name.clone(), VaultConfig::default()) - .await? - .get() - .await?; + let cli_state = CliState::test().await?; - let identity_name = random_name(); + let node_name = random_name(); + cli_state.create_node(&node_name).await.unwrap(); // Premise: we need an identity and a credential before the node manager starts. - // Since the LMDB can trigger some race conditions, we first use the memory storage - // export the identity and credentials,then import in the LMDB after secure-channel - // has been re-created - let secure_channels = SecureChannels::builder() - .with_vault(vault) - .with_identities_repository(cli_state.identities.identities_repository().await?) - .with_identities_storage(InMemoryStorage::create()) - .build(); - - let identifier = create_random_identity(&secure_channels).await?; - - let exported_identity = secure_channels - .identities() - .get_identity(&identifier) - .await? - .export()?; + let identifier = cli_state.get_node(&node_name).await?.identifier(); + let identity = cli_state.get_identity(&identifier).await?; let attributes = AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA) .with_attribute(TRUST_CONTEXT_ID.to_vec(), b"test_trust_context_id".to_vec()) .build(); - let credential = secure_channels - .identities() + let vault = cli_state.get_default_named_vault().await?.vault().await?; + let identities = cli_state.make_identities(vault).await?; + + let credential = identities .credentials() .credentials_creation() .issue_credential( @@ -468,14 +444,19 @@ pub mod test_utils { .await .unwrap(); - drop(secure_channels); - - let config = IdentityConfig::new(&identifier).await; - cli_state.identities.create(&identity_name, config).unwrap(); + cli_state + .store_credential("credential", &identity, credential) + .await?; - let node_name = random_name(); - let node_config = NodeConfig::try_from(&cli_state).unwrap(); - cli_state.nodes.create(&node_name, node_config)?; + let trust_context = cli_state + .create_trust_context( + Some("trust-context".to_string()), + None, + Some("credential".to_string()), + None, + None, + ) + .await?; let node_manager = InMemoryNode::new( context, @@ -484,46 +465,23 @@ pub mod test_utils { FlowControls::generate_flow_control_id(), // FIXME tcp.async_try_clone().await?, ), - NodeManagerTrustOptions::new(Some(TrustContextConfig::new( - "test_trust_context".to_string(), - Some(TrustAuthorityConfig::new( - hex::encode(&exported_identity), - Some(CredentialRetrieverConfig::FromMemory(minicbor::to_vec( - &credential, - )?)), - )), - ))), + NodeManagerTrustOptions::new(Some(trust_context)), ) .await?; + let node_manager = Arc::new(node_manager); let node_manager_worker = NodeManagerWorker::new(node_manager.clone()); - let secure_channels = node_manager.secure_channels.clone(); - - // Import identity, since it doesn't exist in the LMDB storage - let _ = secure_channels - .identities() - .identities_creation() - .import(Some(&identifier), &exported_identity) - .await?; context .start_worker(NODEMANAGER_ADDR, node_manager_worker) .await?; + let secure_channels = node_manager.secure_channels(); Ok(NodeManagerHandle { cli_state, node_manager, tcp: tcp.async_try_clone().await?, - secure_channels: secure_channels.clone(), - identifier, + secure_channels, }) } - - async fn create_random_identity(secure_channels: &Arc) -> Result { - secure_channels - .identities() - .identities_creation() - .create_identity() - .await - } } diff --git a/implementations/rust/ockam/ockam_api/tests/auth_smoke.rs b/implementations/rust/ockam/ockam_api/tests/auth_smoke.rs index 1b4e18146a0..311c8115c08 100644 --- a/implementations/rust/ockam/ockam_api/tests/auth_smoke.rs +++ b/implementations/rust/ockam/ockam_api/tests/auth_smoke.rs @@ -1,4 +1,4 @@ -use ockam::identity::{Identifier, IdentityAttributesReader}; +use ockam::identity::{Identifier, IdentityAttributesRepository}; use ockam_api::auth; use ockam_api::auth::AuthorizationApi; use ockam_api::bootstrapped_identities_store::PreTrustedIdentities; @@ -15,7 +15,7 @@ async fn auth_smoke(ctx: &mut Context) -> Result<()> { "I224ed0b2e5a2be82e267ead6b3279f683616b66da1b2c3d4e5f6a6b5c4d3e2f1":{"attr":"value2"} }"#, )?; - let s: Arc = Arc::new(s); + let s: Arc = Arc::new(s); ctx.start_worker("auth", auth::Server::new(s)).await?; let client = Client::new(&route!["auth"], None); diff --git a/implementations/rust/ockam/ockam_api/tests/authority.rs b/implementations/rust/ockam/ockam_api/tests/authority.rs index da2c1422a31..57e1cb0115a 100644 --- a/implementations/rust/ockam/ockam_api/tests/authority.rs +++ b/implementations/rust/ockam/ockam_api/tests/authority.rs @@ -2,11 +2,13 @@ use ockam::identity::utils::now; use ockam::identity::{secure_channels, AttributesEntry, Identifier, SecureChannels}; use ockam::AsyncTryClone; use ockam_api::authenticator::enrollment_tokens::Members; +use ockam_api::authority_node; use ockam_api::authority_node::{Authority, Configuration}; use ockam_api::bootstrapped_identities_store::PreTrustedIdentities; use ockam_api::cloud::AuthorityNode; +use ockam_api::config::lookup::InternetAddress; +use ockam_api::nodes::service::default_address::DefaultAddress; use ockam_api::nodes::NodeManager; -use ockam_api::{authority_node, DefaultAddress}; use ockam_core::{Address, Result}; use ockam_multiaddr::MultiAddr; use ockam_node::Context; @@ -37,7 +39,7 @@ async fn authority_starts_with_default_configuration(ctx: &mut Context) -> Resul async fn controlling_authority_by_member_times_out(ctx: &mut Context) -> Result<()> { use std::collections::HashMap; - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let admins = setup(ctx, secure_channels.clone(), 1).await?; let admin = &admins[0]; @@ -86,7 +88,7 @@ async fn controlling_authority_by_member_times_out(ctx: &mut Context) -> Result< #[ockam_macros::test] async fn one_admin_test_api(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let admins = setup(ctx, secure_channels.clone(), 1).await?; let admin = &admins[0]; @@ -126,7 +128,7 @@ async fn one_admin_test_api(ctx: &mut Context) -> Result<()> { async fn test_one_admin_one_member(ctx: &mut Context) -> Result<()> { use std::collections::HashMap; - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let admins = setup(ctx, secure_channels.clone(), 1).await?; let admin = &admins[0]; @@ -198,7 +200,7 @@ async fn test_one_admin_one_member(ctx: &mut Context) -> Result<()> { async fn two_admins_two_members_exist_in_one_global_scope(ctx: &mut Context) -> Result<()> { use std::collections::HashMap; - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let admins = setup(ctx, secure_channels.clone(), 2).await?; let admin1 = &admins[0]; @@ -319,7 +321,6 @@ async fn two_admins_two_members_exist_in_one_global_scope(ctx: &mut Context) -> // with freshly created Authority Identifier and temporary files for storage and vault async fn default_configuration() -> Result { let storage_path = NamedTempFile::new().unwrap().keep().unwrap().1; - let vault_path = NamedTempFile::new().unwrap().keep().unwrap().1; let port = thread_rng().gen_range(10000..65535); @@ -331,10 +332,9 @@ async fn default_configuration() -> Result { let mut configuration = authority_node::Configuration { identifier: "I4dba4b2e53b2ed95967b3bab350b6c9ad9c624e5a1b2c3d4e5f6a6b5c4d3e2f1" .try_into()?, - storage_path, - vault_path, + database_path: storage_path, project_identifier: "123456".to_string(), - tcp_listener_address: format!("127.0.0.1:{}", port), + tcp_listener_address: InternetAddress::new(&format!("127.0.0.1:{}", port)).unwrap(), secure_channel_listener_name: None, authenticator_name: None, trusted_identities, diff --git a/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs b/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs index 6f7e936beeb..de24e13f42e 100644 --- a/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs +++ b/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs @@ -7,7 +7,9 @@ use ockam::identity::{ SecureChannels, }; use ockam::route; -use ockam_api::bootstrapped_identities_store::{BootstrapedIdentityStore, PreTrustedIdentities}; +use ockam_api::bootstrapped_identities_store::{ + BootstrapedIdentityAttributesStore, PreTrustedIdentities, +}; use ockam_core::api::Request; use ockam_core::compat::collections::{BTreeMap, HashMap}; use ockam_core::compat::sync::Arc; @@ -21,7 +23,7 @@ async fn credential(ctx: &mut Context) -> Result<()> { let auth_worker_addr = Address::random_local(); // create 2 identities to populate the trusted identities - let identities = identities(); + let identities = identities().await?; let auth_identifier = identities.identities_creation().create_identity().await?; let member_identifier = identities.identities_creation().create_identity().await?; let member_identity = identities.get_identity(&member_identifier).await?; @@ -38,20 +40,23 @@ async fn credential(ctx: &mut Context) -> Result<()> { ), )]); - let bootstrapped = BootstrapedIdentityStore::new( + let bootstrapped = BootstrapedIdentityAttributesStore::new( Arc::new(PreTrustedIdentities::from(pre_trusted)), - identities.repository(), + identities.identity_attributes_repository(), ); // Now recreate the identities services with the previous vault // (so that the authority can verify its signature) // and the repository containing the trusted identities let identities = Identities::builder() - .with_identities_repository(Arc::new(bootstrapped)) + .await? + .with_change_history_repository(identities.change_history_repository()) + .with_identity_attributes_repository(Arc::new(bootstrapped)) .with_vault(identities.vault()) .with_purpose_keys_repository(identities.purpose_keys_repository()) .build(); let secure_channels = SecureChannels::builder() + .await? .with_identities(identities.clone()) .build(); let identities_creation = identities.identities_creation(); @@ -65,7 +70,7 @@ async fn credential(ctx: &mut Context) -> Result<()> { ctx.flow_controls() .add_consumer(auth_worker_addr.clone(), &sc_flow_control_id); let auth = CredentialsIssuer::new( - identities.repository(), + identities.identity_attributes_repository(), identities.credentials(), &auth_identifier, "project42".into(), diff --git a/implementations/rust/ockam/ockam_app_lib/Cargo.toml b/implementations/rust/ockam/ockam_app_lib/Cargo.toml index 50695227625..63799efe2bf 100644 --- a/implementations/rust/ockam/ockam_app_lib/Cargo.toml +++ b/implementations/rust/ockam/ockam_app_lib/Cargo.toml @@ -40,6 +40,7 @@ ockam_multiaddr = { path = "../ockam_multiaddr", version = "0.37.0", features = ockam_transport_tcp = { path = "../ockam_transport_tcp", version = "^0.96.0" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sqlx = { version = "0.7.3", features = ["runtime-tokio", "sqlite", "migrate"] } thiserror = "1.0" tokio = { version = "1.34.0", features = ["full"] } tokio-retry = "0.3" @@ -48,5 +49,8 @@ tracing-appender = "0.2.2" tracing-error = "0.2" tracing-subscriber = { version = "0.3.18", features = ["json"] } +[dev-dependencies] +tempfile = { version = "3.8.0" } + [build-dependencies] cbindgen = "0.26" diff --git a/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs b/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs index ba6a10c0b89..cd497b3afaf 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs @@ -1,16 +1,14 @@ -use miette::{miette, IntoDiagnostic, WrapErr}; +use miette::{IntoDiagnostic, WrapErr}; use tracing::{debug, error, info}; -use crate::api::notification::rust::{Kind, Notification}; -use crate::api::state::OrchestratorStatus; use ockam_api::cli_state; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::{add_project_info_to_node_state, update_enrolled_identity, SpaceConfig}; use ockam_api::cloud::project::{Project, Projects}; use ockam_api::cloud::space::{Space, Spaces}; use ockam_api::enroll::enrollment::Enrollment; use ockam_api::enroll::oidc_service::OidcService; +use crate::api::notification::rust::{Kind, Notification}; +use crate::api::state::OrchestratorStatus; use crate::state::{AppState, NODE_NAME, PROJECT_NAME}; use crate::Result; @@ -112,10 +110,8 @@ impl AppState { } let cli_state = self.state().await; - cli_state - .users_info - .overwrite(&user_info.email, user_info.clone())?; - cli_state.users_info.set_default(&user_info.email)?; + cli_state.store_user(&user_info).await?; + cli_state.set_default_user(&user_info.email).await?; // enroll the current user using that token on the controller { @@ -132,7 +128,11 @@ impl AppState { self.publish_state().await; self.retrieve_project(&space).await?; - let identifier = update_enrolled_identity(&cli_state, NODE_NAME) + let cli_state = self.state().await; + let node = cli_state.get_node(NODE_NAME).await?; + let identifier = node.identifier(); + cli_state + .set_identifier_as_enrolled(&identifier) .await .into_diagnostic()?; info!(%identifier, "User enrolled successfully"); @@ -142,17 +142,14 @@ impl AppState { async fn retrieve_space(&self) -> Result { info!("retrieving the user's space"); - let controller = self.controller().await.into_diagnostic()?; + let node_manager = self.node_manager().await; let context = self.context(); // list the spaces that the user can access // and sort them by name to make sure to get the same space every time // if several spaces are available let spaces = { - let mut spaces = controller - .list_spaces(&context) - .await - .map_err(|e| miette!(e))?; + let mut spaces = node_manager.get_spaces(&context).await?; spaces.sort_by(|s1, s2| s1.name.cmp(&s2.name)); spaces }; @@ -163,16 +160,11 @@ impl AppState { Some(space) => space.clone(), None => { let space_name = cli_state::random_name(); - controller - .create_space(&context, space_name, vec![]) - .await - .map_err(|e| miette!(e))? + node_manager + .create_space(&self.context(), &space_name, vec![]) + .await? } }; - self.state() - .await - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; Ok(space) } @@ -181,11 +173,8 @@ impl AppState { info!("retrieving the user project"); let email = self.user_email().await.wrap_err("User info is not valid")?; - let controller = self.controller().await.into_diagnostic()?; - let projects = controller - .list_projects(&self.context()) - .await - .map_err(|e| miette!(e))?; + let node_manager = self.node_manager().await; + let projects = node_manager.get_projects(&self.context()).await?; let admin_project = projects .iter() .filter(|p| p.has_admin_with_email(&email)) @@ -200,18 +189,19 @@ impl AppState { message: "This might take a few minutes".to_string(), }); let ctx = &self.context(); - let project = controller - .create_project(ctx, space.id.to_string(), PROJECT_NAME.to_string(), vec![]) - .await - .map_err(|e| miette!(e))?; - controller.wait_until_project_is_ready(ctx, project).await? + let project = node_manager + .create_project(ctx, &space.id, PROJECT_NAME, vec![]) + .await?; + node_manager + .wait_until_project_is_ready(ctx, project) + .await? } }; - let cli_state = self.state().await; - cli_state - .projects - .overwrite(&project.name, project.clone())?; - add_project_info_to_node_state(NODE_NAME, &cli_state, None).await?; + + self.state() + .await + .set_node_project(&node_manager.node_name(), &Some(project.name())) + .await?; Ok(project) } } diff --git a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/commands.rs b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/commands.rs index 5960e2078f1..54aa9b39e96 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/commands.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/commands.rs @@ -1,17 +1,19 @@ -use crate::background_node::BackgroundNodeClient; -use crate::incoming_services::state::{IncomingService, Port}; -use crate::state::AppState; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + use miette::IntoDiagnostic; +use tracing::{debug, info, warn}; + use ockam_api::address::get_free_address; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::service::portals::Inlets; use ockam_api::ConnectionStatus; use ockam_core::api::Reply; use ockam_multiaddr::MultiAddr; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; -use tracing::{debug, info, warn}; + +use crate::background_node::BackgroundNodeClient; +use crate::incoming_services::state::{IncomingService, Port}; +use crate::state::AppState; impl AppState { pub(crate) async fn refresh_inlets(&self) -> crate::Result<()> { @@ -94,7 +96,7 @@ impl AppState { /// Returns true if the inlet is already connected to the destination node /// if any error occurs, it returns false async fn is_connected(&self, service: &IncomingService, inlet_node_name: &str) -> bool { - if self.state().await.nodes.exists(inlet_node_name) { + if self.state().await.get_node(inlet_node_name).await.is_ok() { if let Ok(mut inlet_node) = self.background_node(inlet_node_name).await { inlet_node.set_timeout(Duration::from_secs(5)); if let Ok(Reply::Successful(inlet)) = inlet_node diff --git a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/mod.rs index 2683298be5f..261b8165cd4 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/mod.rs @@ -2,4 +2,4 @@ mod commands; mod state; pub use state::IncomingServicesState; -pub use state::PersistentIncomingServiceState; +pub use state::PersistentIncomingService; diff --git a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/state.rs b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/state.rs index ae0eaec9bf8..955b2b5b0fa 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/incoming_services/state.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/incoming_services/state.rs @@ -1,19 +1,22 @@ -use crate::state::{AppState, ModelState}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use minicbor::{Decode, Encode}; -use ockam::identity::Identifier; -use ockam_api::cloud::share::InvitationWithAccess; -use ockam_api::identity::EnrollmentTicket; use serde::{Deserialize, Serialize}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use tracing::warn; +use ockam::identity::Identifier; +use ockam_api::cli_state::enrollments::EnrollmentTicket; +use ockam_api::cloud::share::InvitationWithAccess; + +use crate::state::{AppState, ModelState}; + /// A Socket port number pub type Port = u16; #[derive(Clone, Debug, Decode, Encode, Serialize, Deserialize, PartialEq)] #[rustfmt::skip] #[cbor(map)] -pub struct PersistentIncomingServiceState { +pub struct PersistentIncomingService { #[n(1)] pub(crate) invitation_id: String, #[n(2)] pub(crate) enabled: bool, #[n(3)] pub(crate) name: Option, @@ -41,10 +44,7 @@ impl IncomingServicesState { } impl ModelState { - pub(crate) fn upsert_incoming_service( - &mut self, - id: &str, - ) -> &mut PersistentIncomingServiceState { + pub(crate) fn upsert_incoming_service(&mut self, id: &str) -> &mut PersistentIncomingService { match self .incoming_services .iter_mut() @@ -53,7 +53,7 @@ impl ModelState { // we have to use index, see https://github.com/rust-lang/rust/issues/21906 Some(index) => &mut self.incoming_services[index], None => { - self.incoming_services.push(PersistentIncomingServiceState { + self.incoming_services.push(PersistentIncomingService { invitation_id: id.to_string(), enabled: true, name: None, @@ -306,17 +306,17 @@ impl IncomingService { #[cfg(test)] mod tests { - use crate::incoming_services::PersistentIncomingServiceState; - use crate::state::AppState; - use ockam::identity::{Identifier, OneTimeCode}; + use ockam::identity::OneTimeCode; use ockam::Context; + use ockam_api::cli_state::enrollments::EnrollmentTicket; use ockam_api::cli_state::CliState; + use ockam_api::cloud::project::Project; use ockam_api::cloud::share::{ InvitationWithAccess, ReceivedInvitation, RoleInShare, ServiceAccessDetails, ShareScope, }; - use ockam_api::config::lookup::ProjectLookup; - use ockam_api::identity::EnrollmentTicket; - use std::str::FromStr; + + use crate::incoming_services::PersistentIncomingService; + use crate::state::AppState; fn create_invitation_with( service_access_details: Option, @@ -353,23 +353,26 @@ mod tests { shared_node_route: "remote_service_name".to_string(), enrollment_ticket: EnrollmentTicket::new( OneTimeCode::new(), - Some(ProjectLookup { - node_route: None, + Some(Project { id: "project_id".to_string(), name: "project_name".to_string(), - identity_id: Some( - Identifier::from_str( - "I1234561234561234561234561234561234561234a1b2c3d4e5f6a6b5c4d3e2f1", - ) - .unwrap(), - ), - authority: None, - okta: None, + space_name: "space_name".to_string(), + access_route: "route".to_string(), + users: vec![], + space_id: "space_id".to_string(), + identity: None, + authority_access_route: Some("/project/authority_route".to_string()), + authority_identity: Some("81a201583ba20101025835a4028201815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef403f4041a64dd404a051a77a9434a0282018158407754214545cda6e7ff49136f67c9c7973ec309ca4087360a9f844aac961f8afe3f579a72c0c9530f3ff210f02b7c5f56e96ce12ee256b01d7628519800723805".to_string()), + okta_config: None, + confluent_config: None, + version: None, + running: None, + operation_id: None, + user_roles: vec![], }), - None, ) - .hex_encoded() - .unwrap(), + .hex_encoded() + .unwrap(), } } @@ -377,7 +380,7 @@ mod tests { async fn test_inlet_data_from_invitation(context: &mut Context) -> ockam::Result<()> { // in this test we want to validate data loading from the accepted invitation // as well as using the related persistent data - let app_state = AppState::test(context, CliState::test().unwrap()).await; + let app_state = AppState::test(context, CliState::test().await?).await; let mut invitation = create_invitation_with(None); @@ -434,7 +437,7 @@ mod tests { // let's load another invitation, but persistent state for it already exists app_state .model_mut(|m| { - m.incoming_services.push(PersistentIncomingServiceState { + m.incoming_services.push(PersistentIncomingService { invitation_id: "second_invitation_id".to_string(), enabled: false, name: Some("custom_user_name".to_string()), diff --git a/implementations/rust/ockam/ockam_app_lib/src/invitations/commands.rs b/implementations/rust/ockam/ockam_app_lib/src/invitations/commands.rs index e883586a488..dbab0a9597d 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/invitations/commands.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/invitations/commands.rs @@ -1,13 +1,15 @@ -use miette::IntoDiagnostic; use std::net::SocketAddr; use std::str::FromStr; + +use miette::IntoDiagnostic; use tracing::{debug, info, trace, warn}; +use ockam_api::cloud::share::{CreateServiceInvitation, InvitationListKind, Invitations}; + use crate::api::notification::rust::Notification; use crate::api::notification::Kind; use crate::invitations::state::ReceivedInvitationStatus; use crate::state::{AppState, StateKind, PROJECT_NAME}; -use ockam_api::cloud::share::{CreateServiceInvitation, InvitationListKind, Invitations}; impl AppState { /// Fetch received, accept and sent invitations from the orchestrator @@ -108,7 +110,7 @@ impl AppState { debug!(?i, "Invitation is in status {s:?}, skipping..."); Ok(()) } - } + }; } } } diff --git a/implementations/rust/ockam/ockam_app_lib/src/invitations/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/invitations/mod.rs index f5e0910ba79..ca39e3cf61a 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/invitations/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/invitations/mod.rs @@ -1,14 +1,15 @@ -pub(crate) mod commands; -pub(crate) mod state; - use std::net::SocketAddr; + use tracing::{debug, warn}; +use ockam_api::cli_state::enrollments::EnrollmentTicket; +use ockam_api::cloud::share::CreateServiceInvitation; + use crate::state::{AppState, NODE_NAME}; use crate::Error; -use ockam_api::cli_state::StateDirTrait; -use ockam_api::cloud::share::CreateServiceInvitation; -use ockam_api::identity::EnrollmentTicket; + +pub(crate) mod commands; +pub(crate) mod state; impl AppState { pub(crate) async fn build_args_for_create_service_invitation( @@ -33,15 +34,15 @@ impl AppState { .ok_or::( format!("The outlet {outlet_socket_addr} wasn't found in the App state").into(), )??; - let project = cli_state.projects.default()?; + let project = cli_state.get_default_project().await?; Ok(CreateServiceInvitation::new( &cli_state, None, project.name(), - recipient_email, - NODE_NAME, - service_route.to_string().as_str(), + recipient_email.to_string(), + NODE_NAME.to_string(), + service_route.to_string(), enrollment_ticket, ) .await?) diff --git a/implementations/rust/ockam/ockam_app_lib/src/log.rs b/implementations/rust/ockam/ockam_app_lib/src/log.rs index c916992fe38..824bced243f 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/log.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/log.rs @@ -20,7 +20,6 @@ impl AppState { .runtime() .block_on(async move { this.state().await }); state - .nodes .stdout_logs(NODE_NAME) .expect("Failed to get stdout log path for node") }; diff --git a/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs b/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs index 58a42f7ba14..515243f7ce2 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs @@ -4,14 +4,15 @@ use std::time::Duration; use tracing::{debug, info, trace, warn}; use ockam_api::authenticator::enrollment_tokens::TokenIssuer; -use ockam_api::cloud::project::Projects; -use ockam_api::config::lookup::{ProjectAuthority, ProjectLookup}; -use ockam_api::{cli_state::StateDirTrait, cloud::project::Project, identity::EnrollmentTicket}; +use ockam_api::cli_state::enrollments::EnrollmentTicket; +use ockam_api::cloud::project::{Project, Projects}; -use super::error::{Error, Result}; -use crate::projects::error::Error::{InternalFailure, ListingFailed}; +use crate::projects::error::Error::ListingFailed; use crate::state::{AppState, StateKind}; +use super::error::{Error, Result}; + +// Store the user's admin projects impl AppState { pub(crate) async fn create_enrollment_ticket( &self, @@ -25,16 +26,10 @@ impl AppState { .find(|p| p.id == project_id) .ok_or_else(|| Error::ProjectNotFound(project_id.to_owned()))? .clone(); - let project_authority = ProjectAuthority::from_project(&project) - .await - .into_diagnostic()? - .ok_or(Error::ProjectInvalidState( - "project has no authority set".to_string(), - ))?; let authority_node = self .authority_node( - project_authority.identity_id(), - project_authority.address(), + &project.authority_identifier().await.into_diagnostic()?, + &project.authority_access_route().into_diagnostic()?, None, ) .await @@ -47,9 +42,7 @@ impl AppState { None, ) .await?; - let project_lookup = ProjectLookup::from_project(&project).await.ok(); - let trust_context = project.try_into().ok(); - Ok(EnrollmentTicket::new(otc, project_lookup, trust_context)) + Ok(EnrollmentTicket::new(otc, Some(project))) } pub(crate) async fn refresh_projects(&self) -> Result<()> { @@ -65,12 +58,9 @@ impl AppState { } }; - let controller = self - .controller() - .await - .map_err(|e| InternalFailure(e.to_string()))?; - let projects = controller - .list_projects(&self.context()) + let node_manager = self.node_manager().await; + let projects = node_manager + .get_projects(&self.context()) .await .map_err(|e| ListingFailed(e.to_string()))? .into_iter() @@ -79,13 +69,6 @@ impl AppState { debug!("Projects fetched"); trace!(?projects); - let cli_projects = self.state().await.projects; - for project in &projects { - cli_projects - .overwrite(&project.name, project.clone()) - .map_err(|_| Error::StateSaveFailed)?; - } - *self.projects().write().await = projects; self.mark_as_loaded(StateKind::Projects); self.publish_state().await; diff --git a/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs b/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs index 797faafbeda..7e619e373b1 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/shared_service/relay/create.rs @@ -1,15 +1,18 @@ -use crate::api::state::OrchestratorStatus; -use crate::state::AppState; -use crate::Result; +use std::str::FromStr; +use std::sync::Arc; + use miette::IntoDiagnostic; +use tracing::{debug, info, trace, warn}; + use ockam::Context; -use ockam_api::cli_state::{CliState, StateDirTrait}; +use ockam_api::cli_state::CliState; use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::InMemoryNode; use ockam_multiaddr::MultiAddr; -use std::str::FromStr; -use std::sync::Arc; -use tracing::{debug, info, trace, warn}; + +use crate::api::state::OrchestratorStatus; +use crate::state::AppState; +use crate::Result; impl AppState { /// Try to create a relay until it succeeds. @@ -66,7 +69,7 @@ impl AppState { node_manager: Arc, ) -> Result<()> { trace!("Creating relay"); - match cli_state.projects.default() { + match cli_state.get_default_project().await { Ok(project) => { if let Some(_relay) = get_relay(&node_manager, cli_state).await? { debug!(project = %project.name(), "Relay already exists"); @@ -83,7 +86,7 @@ impl AppState { .create_relay( context, &project_address, - Some(bare_relay_name(cli_state)?), + Some(bare_relay_name(cli_state).await?), false, None, ) @@ -108,7 +111,7 @@ async fn delete_relay( node_manager: &InMemoryNode, cli_state: &CliState, ) -> ockam::Result> { - let relay_name = relay_name(cli_state)?; + let relay_name = relay_name(cli_state).await?; node_manager.delete_relay(&context, &relay_name).await } @@ -116,7 +119,7 @@ async fn get_relay( node_manager: &InMemoryNode, cli_state: &CliState, ) -> ockam::Result> { - let relay_name = relay_name(cli_state)?; + let relay_name = relay_name(cli_state).await?; Ok(node_manager .get_relays() .await @@ -124,15 +127,15 @@ async fn get_relay( .find(|r| r.remote_address() == relay_name)) } -fn relay_name(cli_state: &CliState) -> ockam::Result { - let bare_relay_name = bare_relay_name(cli_state)?; +async fn relay_name(cli_state: &CliState) -> ockam::Result { + let bare_relay_name = bare_relay_name(cli_state).await?; Ok(format!("forward_to_{bare_relay_name}")) } -fn bare_relay_name(cli_state: &CliState) -> ockam::Result { +async fn bare_relay_name(cli_state: &CliState) -> ockam::Result { Ok(cli_state - .identities - .get_or_default(None)? + .get_default_named_identity() + .await? .identifier() .to_string()) } diff --git a/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/state.rs b/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/state.rs index 54cddac4da8..fca2039c433 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/state.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/state.rs @@ -1,6 +1,9 @@ +use tracing::{debug, error}; + +#[cfg(test)] +use crate::incoming_services::PersistentIncomingService; use crate::state::{AppState, ModelState}; use ockam_api::nodes::models::portal::OutletStatus; -use tracing::{debug, error}; impl ModelState { pub fn add_tcp_outlet(&mut self, status: OutletStatus) { @@ -14,12 +17,17 @@ impl ModelState { pub fn get_tcp_outlets(&self) -> &[OutletStatus] { &self.tcp_outlets } + + #[cfg(test)] + pub fn add_incoming_service(&mut self, service: PersistentIncomingService) { + self.incoming_services.push(service); + } } impl AppState { pub(crate) async fn restore_tcp_outlets(&self) { let cli_state = self.state().await; - if !cli_state.is_enrolled().unwrap_or(false) { + if !cli_state.is_enrolled().await.ok().unwrap_or(false) { debug!("Not enrolled, skipping outlet restoration"); return; } diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs index ff4c00941ed..de7335725e1 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs @@ -1,8 +1,3 @@ -mod kind; -mod model; -mod repository; -mod tasks; - use std::sync::{Arc, Mutex, OnceLock}; use std::time::Duration; @@ -11,41 +6,44 @@ use tokio::sync::RwLock; use tracing::{error, info, trace, warn}; use tracing_appender::non_blocking::WorkerGuard; -use crate::api::notification::rust::{Notification, NotificationCallback}; -use crate::api::state::rust::{ - ApplicationState, ApplicationStateCallback, Invitation, Invitee, LocalService, Service, - ServiceGroup, -}; -use crate::background_node::{BackgroundNodeClient, Cli}; -use crate::invitations::state::{InvitationState, ReceivedInvitationStatus}; -pub(crate) use crate::state::model::ModelState; -pub(crate) use crate::state::repository::{LmdbModelStateRepository, ModelStateRepository}; +pub use kind::StateKind; use ockam::identity::Identifier; use ockam::Context; use ockam::{NodeBuilder, TcpListenerOptions, TcpTransport}; -use ockam_api::cli_state::{ - add_project_info_to_node_state, init_node_state, CliState, StateDirTrait, StateItemTrait, -}; +use ockam_api::cli_state::CliState; use ockam_api::cloud::enroll::auth0::UserInfo; use ockam_api::cloud::project::Project; use ockam_api::cloud::{AuthorityNode, Controller}; use ockam_api::nodes::models::portal::OutletStatus; -use ockam_api::nodes::models::transport::{CreateTransportJson, TransportMode, TransportType}; use ockam_api::nodes::service::{ NodeManagerGeneralOptions, NodeManagerTransportOptions, NodeManagerTrustOptions, }; use ockam_api::nodes::{BackgroundNode, InMemoryNode, NodeManagerWorker, NODEMANAGER_ADDR}; -use ockam_api::trust_context::TrustContextConfigBuilder; use ockam_multiaddr::MultiAddr; +use crate::api::notification::rust::{Notification, NotificationCallback}; +use crate::api::state::rust::{ + ApplicationState, ApplicationStateCallback, Invitation, Invitee, LocalService, Service, + ServiceGroup, +}; use crate::api::state::OrchestratorStatus; +use crate::background_node::{BackgroundNodeClient, Cli}; use crate::incoming_services::IncomingServicesState; +use crate::invitations::state::{InvitationState, ReceivedInvitationStatus}; use crate::scheduler::Scheduler; +pub(crate) use crate::state::model::ModelState; +use crate::state::model_state_repository::ModelStateRepository; +pub(crate) use crate::state::model_state_repository_sql::ModelStateSqlxDatabase; use crate::state::tasks::{ RefreshInletsTask, RefreshInvitationsTask, RefreshProjectsTask, RefreshRelayTask, }; use crate::{api, Result}; -pub use kind::StateKind; + +mod kind; +mod model; +mod model_state_repository; +mod model_state_repository_sql; +mod tasks; pub const NODE_NAME: &str = "ockam_app"; // TODO: static project name of "default" is an unsafe default behavior due to backend uniqueness requirements @@ -99,7 +97,8 @@ impl AppState { application_state_callback: ApplicationStateCallback, notification_callback: NotificationCallback, ) -> Result { - let cli_state = CliState::initialize()?; + let cli_state = + CliState::with_default_dir().expect("Failed to load the local Ockam configuration"); let (context, mut executor) = NodeBuilder::new().no_logging().build(); let context = Arc::new(context); @@ -146,7 +145,6 @@ impl AppState { let model_state = model_state_repository .load() .await - .expect("Failed to load the model state") .unwrap_or(ModelState::default()); info!("AppState initialized"); @@ -288,16 +286,11 @@ impl AppState { let mut writer = self.model_state.write().await; *writer = ModelState::default(); } - let identity_path = self - .state() - .await - .identities - .identities_repository_path() - .expect("Failed to get the identities repository path"); - let new_state_repository = LmdbModelStateRepository::new(identity_path).await?; + let cli_state = &self.state().await; + let new_state_repository = create_model_state_repository(cli_state).await; { let mut writer = self.model_state_repository.write().await; - *writer = Arc::new(new_state_repository); + *writer = new_state_repository; } self.update_orchestrator_status(OrchestratorStatus::default()); self.publish_state().await; @@ -307,7 +300,7 @@ impl AppState { async fn reset_state(&self) -> miette::Result<()> { let mut state = self.state.write().await; - match state.reset().await { + match state.recreate().await { Ok(s) => { *state = s; info!("reset the cli state"); @@ -397,15 +390,15 @@ impl AppState { } pub async fn background_node(&self, node_name: &str) -> Result { - Ok(BackgroundNode::create(&self.context(), &self.state().await, node_name).await?) + Ok(BackgroundNode::create_to_node(&self.context(), &self.state().await, node_name).await?) } pub async fn delete_background_node(&self, node_name: &str) -> Result<()> { - Ok(self.state().await.nodes.delete(node_name)?) + Ok(self.state().await.delete_node(node_name, true).await?) } pub async fn is_enrolled(&self) -> Result { - self.state().await.is_enrolled().map_err(|e| { + self.state().await.is_enrolled().await.map_err(|e| { warn!(%e, "Failed to check if user is enrolled"); e.into() }) @@ -418,14 +411,7 @@ impl AppState { } pub async fn user_info(&self) -> Result { - Ok(self - .state - .read() - .await - .users_info - .default()? - .config() - .clone()) + Ok(self.state.read().await.get_default_user().await?) } pub async fn user_email(&self) -> Result { @@ -684,8 +670,6 @@ pub(crate) async fn make_node_manager( ctx: Arc, cli_state: &CliState, ) -> miette::Result> { - init_node_state(cli_state, NODE_NAME, None, None).await?; - let tcp = TcpTransport::create(&ctx).await.into_diagnostic()?; let options = TcpListenerOptions::new(); let listener = tcp @@ -693,20 +677,9 @@ pub(crate) async fn make_node_manager( .await .into_diagnostic()?; - add_project_info_to_node_state(NODE_NAME, cli_state, None).await?; - - let node_state = cli_state.nodes.get(NODE_NAME)?; - node_state.set_setup( - &node_state.config().setup_mut().set_api_transport( - CreateTransportJson::new( - TransportType::Tcp, - TransportMode::Listen, - &listener.socket_address().to_string(), - ) - .into_diagnostic()?, - ), - )?; - let trust_context_config = TrustContextConfigBuilder::new(cli_state).build(); + let _ = cli_state + .create_node_with_optional_values(NODE_NAME, &None, &None) + .await?; let node_manager = Arc::new( InMemoryNode::new( @@ -719,7 +692,7 @@ pub(crate) async fn make_node_manager( true, ), NodeManagerTransportOptions::new(listener.flow_control_id().clone(), tcp), - NodeManagerTrustOptions::new(trust_context_config), + NodeManagerTrustOptions::new(cli_state.get_default_trust_context().await.ok()), ) .await .into_diagnostic()?, @@ -737,13 +710,10 @@ pub(crate) async fn make_node_manager( /// Create the repository containing the model state async fn create_model_state_repository(state: &CliState) -> Arc { - let identity_path = state - .identities - .identities_repository_path() - .expect("Failed to get the identities repository path"); + let database_path = state.database_path(); - match LmdbModelStateRepository::new(identity_path).await { - Ok(model_state_repository) => Arc::new(model_state_repository), + match ModelStateSqlxDatabase::create_at(database_path).await { + Ok(model_state_repository) => model_state_repository, Err(e) => { error!(%e, "Cannot create a model state repository manager"); panic!("Cannot create a model state repository manager: {e:?}"); diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/model.rs b/implementations/rust/ockam/ockam_app_lib/src/state/model.rs index 422f744a57f..ddd736813c5 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/model.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/model.rs @@ -1,4 +1,4 @@ -use crate::incoming_services::PersistentIncomingServiceState; +use crate::incoming_services::PersistentIncomingService; use ockam_api::nodes::models::portal::OutletStatus; use serde::{Deserialize, Serialize}; @@ -9,7 +9,7 @@ pub struct ModelState { pub(crate) tcp_outlets: Vec, #[serde(default = "Vec::new")] - pub(crate) incoming_services: Vec, + pub(crate) incoming_services: Vec, } impl Default for ModelState { @@ -21,7 +21,7 @@ impl Default for ModelState { impl ModelState { pub fn new( tcp_outlets: Vec, - incoming_services: Vec, + incoming_services: Vec, ) -> Self { Self { tcp_outlets, diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository.rs b/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository.rs new file mode 100644 index 00000000000..bbb0d79ff7a --- /dev/null +++ b/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository.rs @@ -0,0 +1,15 @@ +use ockam_core::async_trait; + +use crate::state::model::ModelState; +use crate::Result; + +/// The ModelStateRepository is responsible for storing and loading +/// the persistent data managed by the desktop application. +#[async_trait] +pub trait ModelStateRepository: Send + Sync + 'static { + /// Store / update the full model state in the database + async fn store(&self, model_state: &ModelState) -> Result<()>; + + /// Load the model state from the database + async fn load(&self) -> Result; +} diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository_sql.rs b/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository_sql.rs new file mode 100644 index 00000000000..2baca83f977 --- /dev/null +++ b/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository_sql.rs @@ -0,0 +1,207 @@ +use std::net::SocketAddr; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::*; +use tracing::debug; + +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_api::nodes::models::portal::OutletStatus; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Error; +use ockam_core::{async_trait, Address}; + +use crate::incoming_services::PersistentIncomingService; +use crate::state::model::ModelState; +use crate::state::model_state_repository::ModelStateRepository; +use crate::Result; + +#[derive(Clone)] +pub struct ModelStateSqlxDatabase { + database: Arc, +} + +impl ModelStateSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for model state"); + Self { database } + } + + /// Create a database on the specified path + pub async fn create_at>(path: P) -> Result> { + Ok(Arc::new(Self::new(Arc::new( + SqlxDatabase::create(path).await?, + )))) + } + + /// Create a new in-memory database + #[allow(unused)] + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("model state").await?, + ))) + } +} + +/// The implementation simply serializes / deserializes the ModelState as JSON +#[async_trait] +impl ModelStateRepository for ModelStateSqlxDatabase { + async fn store(&self, model_state: &ModelState) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + + for tcp_outlet_status in &model_state.tcp_outlets { + let query = query("INSERT OR REPLACE INTO tcp_outlet_status VALUES (?, ?, ?, ?)") + .bind(tcp_outlet_status.alias.to_sql()) + .bind(tcp_outlet_status.socket_addr.to_sql()) + .bind(tcp_outlet_status.worker_addr.to_sql()) + .bind(tcp_outlet_status.payload.as_ref().map(|p| p.to_sql())); + query.execute(&self.database.pool).await.void()?; + } + + for incoming_service in &model_state.incoming_services { + let query = query("INSERT OR REPLACE INTO incoming_service VALUES (?, ?, ?)") + .bind(incoming_service.invitation_id.to_sql()) + .bind(incoming_service.enabled.to_sql()) + .bind(incoming_service.name.as_ref().map(|n| n.to_sql())); + query.execute(&self.database.pool).await.void()?; + } + transaction.commit().await.void()?; + + Ok(()) + } + + async fn load(&self) -> Result { + let query1 = query_as("SELECT * FROM tcp_outlet_status"); + let result: Vec = + query1.fetch_all(&self.database.pool).await.into_core()?; + let tcp_outlets = result + .into_iter() + .map(|r| r.tcp_outlet_status()) + .collect::>>()?; + + let query2 = query_as("SELECT * FROM incoming_service"); + let result: Vec = + query2.fetch_all(&self.database.pool).await.into_core()?; + let incoming_services = result + .into_iter() + .map(|r| r.persistent_incoming_service()) + .collect::>>()?; + Ok(ModelState::new(tcp_outlets, incoming_services)) + } +} + +// Database serialization / deserialization + +/// Low-level representation of a row in the tcp_outlet_status table +#[derive(sqlx::FromRow)] +struct TcpOutletStatusRow { + alias: String, + socket_addr: String, + worker_addr: String, + payload: Option, +} + +impl TcpOutletStatusRow { + fn tcp_outlet_status(&self) -> Result { + let socket_addr = SocketAddr::from_str(&self.socket_addr) + .map_err(|e| Error::new(Origin::Application, Kind::Serialization, e.to_string()))?; + let worker_addr = Address::from_string(&self.worker_addr); + Ok(OutletStatus { + alias: self.alias.clone(), + socket_addr, + worker_addr, + payload: self.payload.clone(), + }) + } +} + +/// Low-level representation of a row in the incoming_service table +#[derive(sqlx::FromRow)] +struct PersistentIncomingServiceRow { + invitation_id: String, + enabled: bool, + name: Option, +} + +impl PersistentIncomingServiceRow { + fn persistent_incoming_service(&self) -> Result { + Ok(PersistentIncomingService { + invitation_id: self.invitation_id.clone(), + enabled: self.enabled, + name: self.name.clone(), + }) + } +} + +#[cfg(test)] +mod tests { + use ockam_api::nodes::models::portal::OutletStatus; + use ockam_core::Address; + + use super::*; + + #[tokio::test] + async fn store_and_load() -> Result<()> { + let db = create_database().await?; + let repository = create_repository(db.clone()); + + let mut state = ModelState::default(); + repository.store(&state).await?; + let loaded = repository.load().await?; + assert!(state.tcp_outlets.is_empty()); + assert_eq!(state, loaded); + + // Add a tcp outlet + state.add_tcp_outlet(OutletStatus::new( + "127.0.0.1:1001".parse()?, + Address::from_string("s1"), + "s1", + None, + )); + // Add an incoming service + state.add_incoming_service(PersistentIncomingService { + invitation_id: "1235".to_string(), + enabled: true, + name: Some("aws".to_string()), + }); + repository.store(&state).await?; + let loaded = repository.load().await?; + assert_eq!(state.tcp_outlets.len(), 1); + assert_eq!(state.incoming_services.len(), 1); + assert_eq!(state, loaded); + + // Add a few more + for i in 2..=5 { + state.add_tcp_outlet(OutletStatus::new( + format!("127.0.0.1:100{i}").parse().unwrap(), + Address::from_string(format!("s{i}")), + &format!("s{i}"), + None, + )); + repository.store(&state).await.unwrap(); + } + let loaded = repository.load().await?; + assert_eq!(state.tcp_outlets.len(), 5); + assert_eq!(state, loaded); + + // Reload from DB scratch to emulate an app restart + let repository = create_repository(db); + let loaded = repository.load().await?; + assert_eq!(state.tcp_outlets.len(), 5); + assert_eq!(state.incoming_services.len(), 1); + assert_eq!(state, loaded); + + Ok(()) + } + + /// HELPERS + fn create_repository(db: Arc) -> Arc { + Arc::new(ModelStateSqlxDatabase::new(db)) + } + + async fn create_database() -> Result> { + Ok(SqlxDatabase::in_memory("enrollments-test").await?) + } +} diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs b/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs deleted file mode 100644 index 992678f69e8..00000000000 --- a/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::state::model::ModelState; -use miette::miette; -use ockam::identity::storage::Storage; -use ockam::LmdbStorage; -use ockam_core::async_trait; -use std::path::Path; -use tracing::trace; - -use crate::Result; - -const MODEL_STATE_ID: &str = "model_state"; -const MODEL_STATE_KEY: &str = "model_state_key"; - -/// The ModelStateRepository is responsible for storing and loading -/// ModelState data (user information, shared services etc...) -/// The state must be stored everytime it is modified (see set_user_info in AppState for example) -/// so that it can be loaded again when the application starts up -#[async_trait] -pub trait ModelStateRepository: Send + Sync + 'static { - async fn store(&self, model_state: &ModelState) -> Result<()>; - async fn load(&self) -> Result>; -} - -/// This implementation of the ModelStateRepository piggy-backs for now on the LMDB storage -/// which is used to store all the data related to identities. -/// We will possibly store all data eventually using SQLite and in that case the ModelData -/// can be a set of tables dedicated to the desktop application -pub struct LmdbModelStateRepository { - storage: LmdbStorage, -} - -impl LmdbModelStateRepository { - pub async fn new>(path: P) -> Result { - Ok(Self { - storage: LmdbStorage::new(path).await.map_err(|e| miette!(e))?, - }) - } -} - -/// The implementation simply serializes / deserializes the ModelState as JSON -#[async_trait] -impl ModelStateRepository for LmdbModelStateRepository { - async fn store(&self, model_state: &ModelState) -> Result<()> { - self.storage - .set( - MODEL_STATE_ID, - MODEL_STATE_KEY.to_string(), - serde_json::to_vec(model_state)?, - ) - .await - .map_err(|e| miette!(e))?; - trace!(?model_state, "stored model state"); - Ok(()) - } - - async fn load(&self) -> Result> { - match self.storage.get(MODEL_STATE_ID, MODEL_STATE_KEY).await { - Err(e) => Err(miette!(e).into()), - Ok(None) => Ok(None), - Ok(Some(bytes)) => { - let state = serde_json::from_slice(bytes.as_slice()).map_err(|e| miette!(e))?; - trace!(?state, "loaded model state"); - Ok(state) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ockam_api::nodes::models::portal::OutletStatus; - use ockam_core::Address; - - #[tokio::test] - async fn store_and_load_tcp_outlets() { - let path = std::env::temp_dir().join("ockam_app_lib_test"); - let _ = std::fs::remove_dir_all(&path); - - // Initial state - let repo = LmdbModelStateRepository::new(&path).await.unwrap(); - let mut state = ModelState::default(); - repo.store(&state).await.unwrap(); - let loaded = repo.load().await.unwrap().unwrap(); - assert!(state.tcp_outlets.is_empty()); - assert_eq!(state, loaded); - - // Add a tcp outlet - state.add_tcp_outlet(OutletStatus::new( - "127.0.0.1:1001".parse().unwrap(), - Address::from_string("s1"), - "s1", - None, - )); - repo.store(&state).await.unwrap(); - let loaded = repo.load().await.unwrap().unwrap(); - assert_eq!(state.tcp_outlets.len(), 1); - assert_eq!(state, loaded); - - // Add a few more - for i in 2..=5 { - state.add_tcp_outlet(OutletStatus::new( - format!("127.0.0.1:100{i}").parse().unwrap(), - Address::from_string(format!("s{i}")), - &format!("s{i}"), - None, - )); - repo.store(&state).await.unwrap(); - } - let loaded = repo.load().await.unwrap().unwrap(); - assert_eq!(state.tcp_outlets.len(), 5); - assert_eq!(state, loaded); - - // Reload from DB scratch to emulate an app restart - let repo = LmdbModelStateRepository::new(&path).await.unwrap(); - let loaded = repo.load().await.unwrap().unwrap(); - assert_eq!(state.tcp_outlets.len(), 5); - assert_eq!(state, loaded); - } -} diff --git a/implementations/rust/ockam/ockam_command/Cargo.toml b/implementations/rust/ockam/ockam_command/Cargo.toml index c68f4457ed6..959af8d5640 100644 --- a/implementations/rust/ockam/ockam_command/Cargo.toml +++ b/implementations/rust/ockam/ockam_command/Cargo.toml @@ -67,6 +67,7 @@ ctrlc = { version = "3.4.1", features = ["termination"] } dialoguer = "0.11.0" duct = "0.13" flate2 = "1.0.28" +futures = "0.3.28" hex = "0.4" home = "0.5" indicatif = "0.17.7" diff --git a/implementations/rust/ockam/ockam_command/src/authenticated.rs b/implementations/rust/ockam/ockam_command/src/authenticated.rs index 2db61f78234..c81d014fd6b 100644 --- a/implementations/rust/ockam/ockam_command/src/authenticated.rs +++ b/implementations/rust/ockam/ockam_command/src/authenticated.rs @@ -1,4 +1,3 @@ -use crate::node::get_node_name; use crate::output::Output; use crate::util::node_rpc; use crate::util::parsers::identity_identifier_parser; @@ -8,7 +7,6 @@ use clap::{Args, Subcommand}; use miette::{miette, Context as _}; use ockam::identity::{AttributesEntry, Identifier}; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::auth::AuthorizationApi; use ockam_api::is_local_node; use ockam_api::nodes::BackgroundNode; @@ -78,9 +76,7 @@ async fn make_background_node_client( addr: &MultiAddr, ) -> Result { is_local_node(addr).context("The address must point to a local node")?; - let to = get_node_name(&opts.state, &Some(addr.to_string())); - let node_name = extract_address_value(&to)?; - Ok(BackgroundNode::create(ctx, &opts.state, &node_name).await?) + Ok(BackgroundNode::create_to_node(ctx, &opts.state, &addr.to_string()).await?) } struct IdentifierWithAttributes { diff --git a/implementations/rust/ockam/ockam_command/src/authority/create.rs b/implementations/rust/ockam/ockam_command/src/authority/create.rs index a3469f8fdb9..d5bea024f74 100644 --- a/implementations/rust/ockam/ockam_command/src/authority/create.rs +++ b/implementations/rust/ockam/ockam_command/src/authority/create.rs @@ -1,25 +1,27 @@ -use crate::node::util::run_ockam; -use crate::util::{embedded_node_that_is_not_stopped, exitcode}; -use crate::util::{local_cmd, node_rpc}; -use crate::{docs, identity, CommandGlobalOpts, Result}; -use clap::{ArgGroup, Args}; -use miette::Context as _; +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; +use std::process; + +use clap::ArgGroup; +use clap::Args; use miette::{miette, IntoDiagnostic}; +use serde::{Deserialize, Serialize}; + use ockam::identity::{AttributesEntry, Identifier}; use ockam::Context; use ockam_api::authority_node; use ockam_api::authority_node::{OktaConfiguration, TrustedIdentity}; use ockam_api::bootstrapped_identities_store::PreTrustedIdentities; -use ockam_api::cli_state::init_node_state; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_api::nodes::models::transport::{CreateTransportJson, TransportMode, TransportType}; -use ockam_api::DefaultAddress; +use ockam_api::config::lookup::InternetAddress; +use ockam_api::nodes::service::default_address::DefaultAddress; use ockam_core::compat::collections::HashMap; use ockam_core::compat::fmt; -use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; -use std::path::PathBuf; -use tracing::debug; + +use crate::node::util::run_ockam; +use crate::util::parsers::internet_address_parser; +use crate::util::{embedded_node_that_is_not_stopped, exitcode}; +use crate::util::{local_cmd, node_rpc}; +use crate::{docs, CommandGlobalOpts, Result}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -28,9 +30,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create an Authority node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] #[clap(group(ArgGroup::new("trusted").required(true).args(& ["trusted_identities", "reload_from_trusted_identities_file"])))] pub struct CreateCommand { @@ -44,13 +46,14 @@ pub struct CreateCommand { /// TCP listener address #[arg( - display_order = 900, - long, - short, - id = "SOCKET_ADDRESS", - default_value = "127.0.0.1:4000" + display_order = 900, + long, + short, + id = "SOCKET_ADDRESS", + default_value = "127.0.0.1:4000", + value_parser = internet_address_parser )] - tcp_listener_address: String, + tcp_listener_address: InternetAddress, /// `authority create` started a child process to run this node in foreground. #[arg(long, hide = true)] @@ -107,14 +110,9 @@ async fn spawn_background_node( opts: &CommandGlobalOpts, cmd: &CreateCommand, ) -> miette::Result<()> { - // Create node state, including the vault and identity if they don't exist - init_node_state( - &opts.state, - &cmd.node_name, - cmd.vault.as_deref(), - cmd.identity.as_deref(), - ) - .await?; + opts.state + .create_node_with_optional_values(&cmd.node_name, &cmd.identity, &None) + .await?; // Construct the arguments list and re-execute the ockam // CLI in foreground mode to start the newly created node @@ -128,7 +126,7 @@ async fn spawn_background_node( "--project-identifier".to_string(), cmd.project_identifier.clone(), "--tcp-listener-address".to_string(), - cmd.tcp_listener_address.clone(), + cmd.tcp_listener_address.to_string(), "--foreground".to_string(), "--child-process".to_string(), ]; @@ -187,7 +185,7 @@ async fn spawn_background_node( } args.push(cmd.node_name.to_string()); - run_ockam(opts, &cmd.node_name, args, cmd.logging_to_file()) + run_ockam(opts, &cmd.node_name, args, cmd.logging_to_file()).await } impl CreateCommand { @@ -259,42 +257,18 @@ async fn start_authority_node( ) -> miette::Result<()> { let (opts, cmd) = args; - // Create node state, including the vault and identity if they don't exist - if !opts.state.nodes.exists(&cmd.node_name) { - init_node_state( - &opts.state, - &cmd.node_name, - cmd.vault.as_deref(), - cmd.identity.as_deref(), - ) - .await?; - }; - // Retrieve the authority identity if it has been created before // otherwise create a new one - let identifier = match &cmd.identity { - Some(identity_name) => { - debug!(name=%identity_name, "getting identity from state"); - opts.state - .identities - .get(identity_name) - .context("Identity not found")? - .config() - .identifier() - } - None => { - debug!("getting default identity from state"); - match opts.state.identities.default() { - Ok(state) => state.config().identifier(), - Err(_) => { - debug!("creating default identity"); - let cmd = identity::CreateCommand::new("authority".into(), None, None); - cmd.create_identity(opts.clone()).await? - } - } - } - }; - debug!(identifier=%identifier, "authority identifier"); + let identity_name = cmd.identity.clone().unwrap_or("authority".to_string()); + let node = opts + .state + .create_node_with_optional_values(&cmd.node_name, &Some(identity_name), &None) + .await?; + opts.state + .set_tcp_listener_address(&node.name(), cmd.tcp_listener_address.to_string()) + .await?; + opts.state.set_as_authority_node(&node.name()).await?; + opts.state.set_node_pid(&node.name(), process::id()).await?; let okta_configuration = match (&cmd.tenant_base_url, &cmd.certificate, &cmd.attributes) { (Some(tenant_base_url), Some(certificate), Some(attributes)) => Some(OktaConfiguration { @@ -306,34 +280,11 @@ async fn start_authority_node( _ => None, }; - // persist the node state and mark it as an authority node - // That flag allows the node to be seen as UP when listing the nodes with the - // the `ockam node list` command, without having to send a TCP query to open a connection - // because this would fail if there is no intention to create a secure channel - debug!("updating node state's setup config"); - let node_state = opts.state.nodes.get(&cmd.node_name)?; - node_state.set_setup( - &node_state - .config() - .setup_mut() - .set_verbose(opts.global_args.verbose) - .set_authority_node() - .set_api_transport( - CreateTransportJson::new( - TransportType::Tcp, - TransportMode::Listen, - cmd.tcp_listener_address.as_str(), - ) - .into_diagnostic()?, - ), - )?; - - let trusted_identities = cmd.trusted_identities(&identifier)?; + let trusted_identities = cmd.trusted_identities(&node.clone().identifier())?; let configuration = authority_node::Configuration { - identifier, - storage_path: opts.state.identities.identities_repository_path()?, - vault_path: opts.state.vaults.default()?.vault_file_path().clone(), + identifier: node.identifier(), + database_path: opts.state.database_path(), project_identifier: cmd.project_identifier, tcp_listener_address: cmd.tcp_listener_address, secure_channel_listener_name: None, @@ -362,20 +313,15 @@ fn parse_trusted_identities(values: &str) -> Result { #[cfg(test)] mod tests { - use super::*; - use ockam::identity::Identifier; + use ockam::identity::{identities, Identifier}; use ockam_core::compat::collections::HashMap; - #[test] - fn test_parse_trusted_identities() { - let identity1 = Identifier::try_from( - "Ie86be15e83d1c93e24dd1967010b01b6df491b45a1b2c3d4e5f6a6b5c4d3e2f1", - ) - .unwrap(); - let identity2 = Identifier::try_from( - "I6c20e814b56579306f55c64e8747e6c1b4a53d9aa1b2c3d4e5f6a6b5c4d3e2f1", - ) - .unwrap(); + use super::*; + + #[tokio::test] + async fn test_parse_trusted_identities() -> Result<()> { + let identity1 = create_identity().await?; + let identity2 = create_identity().await?; let trusted = format!("{{\"{identity1}\": {{\"name\": \"value\", \"trust_context_id\": \"1\"}}, \"{identity2}\": {{\"trust_context_id\" : \"1\", \"ockam-role\" : \"enroller\"}}}}"); let actual = parse_trusted_identities(trusted.as_str()).unwrap(); @@ -388,11 +334,24 @@ mod tests { ("trust_context_id".into(), "1".into()), ("ockam-role".into(), "enroller".into()), ]); - let expected = vec![ + let mut expected = vec![ TrustedIdentity::new(&identity1, &attributes1), TrustedIdentity::new(&identity2, &attributes2), ]; - assert_eq!(actual.trusted_identities(), expected); + expected.sort_by_key(|t| t.identifier()); + + let mut trusted_identities = actual.trusted_identities(); + trusted_identities.sort_by_key(|t| t.identifier()); + + assert_eq!(trusted_identities, expected); + + Ok(()) + } + + /// HELPERS + async fn create_identity() -> Result { + let identities = identities().await?; + Ok(identities.identities_creation().create_identity().await?) } } diff --git a/implementations/rust/ockam/ockam_command/src/configuration/get.rs b/implementations/rust/ockam/ockam_command/src/configuration/get.rs index 69b659d7de2..c4b8ff5f197 100644 --- a/implementations/rust/ockam/ockam_command/src/configuration/get.rs +++ b/implementations/rust/ockam/ockam_command/src/configuration/get.rs @@ -1,7 +1,9 @@ -use crate::util::local_cmd; -use crate::CommandGlobalOpts; use clap::Args; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::CommandGlobalOpts; #[derive(Clone, Debug, Args)] pub struct GetCommand { @@ -11,13 +13,19 @@ pub struct GetCommand { impl GetCommand { pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(options, self)); + node_rpc(run_impl, (options, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: GetCommand) -> miette::Result<()> { - let node_state = opts.state.nodes.get(cmd.alias)?; - let addr = &node_state.config().setup().api_transport()?.addr; +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, GetCommand), +) -> miette::Result<()> { + let node_info = opts.state.get_node(&cmd.alias).await?; + let addr = &node_info + .tcp_listener_address() + .map(|a| a.to_string()) + .unwrap_or("N/A".to_string()); println!("Address: {addr}"); Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/configuration/get_default_node.rs b/implementations/rust/ockam/ockam_command/src/configuration/get_default_node.rs index 1b449321a3d..7572b8b663e 100644 --- a/implementations/rust/ockam/ockam_command/src/configuration/get_default_node.rs +++ b/implementations/rust/ockam/ockam_command/src/configuration/get_default_node.rs @@ -1,6 +1,7 @@ use clap::Args; +use ockam_node::Context; -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::CommandGlobalOpts; #[derive(Clone, Debug, Args)] @@ -8,11 +9,16 @@ pub struct GetDefaultNodeCommand {} impl GetDefaultNodeCommand { pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(options)); + node_rpc(run_impl, options); } } -fn run_impl(_opts: CommandGlobalOpts) -> miette::Result<()> { - // TODO: get from opts.state.nodes().default() - todo!() +async fn run_impl(_ctx: Context, opts: CommandGlobalOpts) -> miette::Result<()> { + let node_info = opts.state.get_default_node().await?; + let addr = &node_info + .tcp_listener_address() + .map(|a| a.to_string()) + .unwrap_or("N/A".to_string()); + println!("Address: {addr}"); + Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/configuration/list.rs b/implementations/rust/ockam/ockam_command/src/configuration/list.rs index 9727cd51668..bad27cf9037 100644 --- a/implementations/rust/ockam/ockam_command/src/configuration/list.rs +++ b/implementations/rust/ockam/ockam_command/src/configuration/list.rs @@ -1,19 +1,21 @@ -use crate::util::local_cmd; -use crate::CommandGlobalOpts; use clap::Args; -use ockam_api::cli_state::StateDirTrait; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::CommandGlobalOpts; #[derive(Clone, Debug, Args)] pub struct ListCommand {} impl ListCommand { pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(options, self)); + node_rpc(run_impl, options); } } -fn run_impl(opts: CommandGlobalOpts, _cmd: ListCommand) -> miette::Result<()> { - for node in opts.state.nodes.list()? { +async fn run_impl(_ctx: Context, opts: CommandGlobalOpts) -> miette::Result<()> { + for node in opts.state.get_nodes().await? { opts.terminal.write(format!("Node: {}\n", node.name()))?; } Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/configuration/set_default_node.rs b/implementations/rust/ockam/ockam_command/src/configuration/set_default_node.rs index dfa0249a213..8c0cf34dfeb 100644 --- a/implementations/rust/ockam/ockam_command/src/configuration/set_default_node.rs +++ b/implementations/rust/ockam/ockam_command/src/configuration/set_default_node.rs @@ -1,6 +1,8 @@ -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::CommandGlobalOpts; use clap::Args; +use miette::IntoDiagnostic; +use ockam_node::Context; #[derive(Clone, Debug, Args)] pub struct SetDefaultNodeCommand { @@ -9,12 +11,17 @@ pub struct SetDefaultNodeCommand { } impl SetDefaultNodeCommand { - pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(&self.name, &options)); + pub fn run(self, opts: CommandGlobalOpts) { + node_rpc(run_impl, (opts, self)); } } -fn run_impl(_name: &str, _options: &CommandGlobalOpts) -> miette::Result<()> { - // TODO: add symlink to options.state.defaults().node - todo!() +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, SetDefaultNodeCommand), +) -> miette::Result<()> { + opts.state + .set_default_node(&cmd.name) + .await + .into_diagnostic() } diff --git a/implementations/rust/ockam/ockam_command/src/credential/get.rs b/implementations/rust/ockam/ockam_command/src/credential/get.rs index 97f5e44af21..52c1f2947dc 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/get.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/get.rs @@ -3,7 +3,7 @@ use clap::Args; use ockam::Context; use ockam_api::nodes::{BackgroundNode, Credentials}; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::util::node_rpc; use crate::CommandGlobalOpts; @@ -22,7 +22,6 @@ pub struct GetCommand { impl GetCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -32,8 +31,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, GetCommand)) -> miet } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: GetCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; node.get_credential(ctx, cmd.overwrite, cmd.identity) .await?; Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/credential/issue.rs b/implementations/rust/ockam/ockam_command/src/credential/issue.rs index 75a7deafe4f..647ffb72739 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/issue.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/issue.rs @@ -1,20 +1,17 @@ -use ockam_core::compat::collections::HashMap; - -use crate::identity::{get_identity_name, initialize_identity_if_default}; -use crate::{ - util::{node_rpc, parsers::identity_identifier_parser}, - vault::default_vault_name, - CommandGlobalOpts, Result, -}; use clap::Args; - -use crate::output::{CredentialAndPurposeKeyDisplay, EncodeFormat}; use miette::{miette, IntoDiagnostic}; + use ockam::identity::utils::AttributesBuilder; use ockam::identity::Identifier; use ockam::identity::{MAX_CREDENTIAL_VALIDITY, PROJECT_MEMBER_SCHEMA, TRUST_CONTEXT_ID}; use ockam::Context; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; +use ockam_core::compat::collections::HashMap; + +use crate::output::{CredentialAndPurposeKeyDisplay, EncodeFormat}; +use crate::{ + util::{node_rpc, parsers::identity_identifier_parser}, + CommandGlobalOpts, Result, +}; #[derive(Clone, Debug, Args)] pub struct IssueCommand { @@ -40,7 +37,6 @@ pub struct IssueCommand { impl IssueCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.as_identity); node_rpc(run_impl, (opts, self)); } @@ -64,23 +60,21 @@ async fn run_impl( _ctx: Context, (opts, cmd): (CommandGlobalOpts, IssueCommand), ) -> miette::Result<()> { - let identity_name = get_identity_name(&opts.state, &cmd.as_identity); - let ident_state = opts.state.identities.get(&identity_name)?; - let auth_identity_identifier = ident_state.config().identifier().clone(); - - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); - let vault = opts.state.vaults.get(&vault_name)?.get().await?; - let identities = opts.state.get_identities(vault).await?; - let issuer = ident_state.identifier(); + let authority = opts + .state + .get_identifier_by_optional_name(&cmd.as_identity) + .await?; + + let vault = opts + .state + .get_named_vault_or_default(&cmd.vault) + .await? + .vault() + .await?; + let identities = opts.state.make_identities(vault).await?; let mut attributes_builder = AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA) - .with_attribute( - TRUST_CONTEXT_ID.to_vec(), - auth_identity_identifier.to_string(), - ); + .with_attribute(TRUST_CONTEXT_ID.to_vec(), authority.to_string()); for (key, value) in cmd.attributes()? { attributes_builder = attributes_builder.with_attribute(key.as_bytes().to_vec(), value.as_bytes().to_vec()); @@ -90,7 +84,7 @@ async fn run_impl( .credentials() .credentials_creation() .issue_credential( - &issuer, + &authority, cmd.identity_identifier(), attributes_builder.build(), MAX_CREDENTIAL_VALIDITY, diff --git a/implementations/rust/ockam/ockam_command/src/credential/list.rs b/implementations/rust/ockam/ockam_command/src/credential/list.rs index 238f3e4a8de..49443f6a11b 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/list.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/list.rs @@ -2,11 +2,8 @@ use clap::{arg, Args}; use colorful::Colorful; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; -use crate::{ - fmt_log, terminal::OckamColor, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, -}; +use crate::{fmt_log, terminal::OckamColor, util::node_rpc, CommandGlobalOpts}; use super::CredentialOutput; @@ -30,15 +27,16 @@ async fn run_impl( opts.terminal .write_line(&fmt_log!("Listing Credentials...\n"))?; - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); + let vault_name = opts + .state + .get_named_vault_or_default(&cmd.vault) + .await? + .name(); let mut credentials: Vec = Vec::new(); - for cred_state in opts.state.credentials.list()? { - let cred = CredentialOutput::try_from_state(&opts, &cred_state, &vault_name).await?; - credentials.push(cred); + for credential in opts.state.get_credentials().await? { + let credential_output = CredentialOutput::new(credential).await; + credentials.push(credential_output); } let list = opts.terminal.build_list( diff --git a/implementations/rust/ockam/ockam_command/src/credential/mod.rs b/implementations/rust/ockam/ockam_command/src/credential/mod.rs index f0bc9680f7b..61a44417e99 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/mod.rs @@ -1,29 +1,25 @@ -pub(crate) mod get; -pub(crate) mod issue; -pub(crate) mod list; -pub(crate) mod present; -pub(crate) mod show; -pub(crate) mod store; -pub(crate) mod verify; - +use clap::{Args, Subcommand}; use colorful::Colorful; + pub(crate) use get::GetCommand; pub(crate) use issue::IssueCommand; pub(crate) use list::ListCommand; -use ockam::identity::{Identifier, Identities}; -use ockam_api::cli_state::{CredentialState, StateItemTrait}; +use ockam_api::cli_state::NamedCredential; pub(crate) use present::PresentCommand; pub(crate) use show::ShowCommand; -use std::sync::Arc; pub(crate) use store::StoreCommand; pub(crate) use verify::VerifyCommand; use crate::output::{CredentialAndPurposeKeyDisplay, Output}; use crate::{CommandGlobalOpts, Result}; -use clap::{Args, Subcommand}; -use miette::IntoDiagnostic; -use ockam::identity::models::CredentialAndPurposeKey; -use ockam_api::cli_state::traits::StateDirTrait; + +pub(crate) mod get; +pub(crate) mod issue; +pub(crate) mod list; +pub(crate) mod present; +pub(crate) mod show; +pub(crate) mod store; +pub(crate) mod verify; /// Manage Credentials #[derive(Clone, Debug, Args)] @@ -59,40 +55,6 @@ impl CredentialCommand { } } -pub async fn identities(vault_name: &str, opts: &CommandGlobalOpts) -> Result> { - let vault = opts.state.vaults.get(vault_name)?.get().await?; - let identities = opts.state.get_identities(vault).await?; - - Ok(identities) -} - -pub async fn identity(identity: &str, identities: Arc) -> Result { - let identity_as_bytes = hex::decode(identity)?; - - let identifier = identities - .identities_creation() - .import(None, &identity_as_bytes) - .await?; - - Ok(identifier) -} - -pub async fn validate_encoded_cred( - encoded_cred: &[u8], - identities: Arc, - issuer: &Identifier, -) -> Result<()> { - let cred: CredentialAndPurposeKey = minicbor::decode(encoded_cred)?; - - identities - .credentials() - .credentials_verification() - .verify_credential(None, &[issuer.clone()], &cred) - .await?; - - Ok(()) -} - pub struct CredentialOutput { name: String, credential: String, @@ -100,33 +62,15 @@ pub struct CredentialOutput { } impl CredentialOutput { - pub async fn try_from_state( - opts: &CommandGlobalOpts, - state: &CredentialState, - vault_name: &str, - ) -> Result { - let config = state.config(); - - let identities = identities(vault_name, opts).await.into_diagnostic()?; - - let is_verified = validate_encoded_cred( - &config.encoded_credential, - identities, - &config.issuer_identifier, - ) - .await - .is_ok(); - - let credential = config.credential()?; - let credential = format!("{}", CredentialAndPurposeKeyDisplay(credential)); - - let output = Self { - name: state.name().to_string(), - credential, - is_verified, - }; - - Ok(output) + pub async fn new(credential: NamedCredential) -> Self { + Self { + name: credential.name(), + credential: format!( + "{}", + CredentialAndPurposeKeyDisplay(credential.credential_and_purpose_key()) + ), + is_verified: true, + } } } diff --git a/implementations/rust/ockam/ockam_command/src/credential/present.rs b/implementations/rust/ockam/ockam_command/src/credential/present.rs index bb5a2a69554..cb9f60cb5d0 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/present.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/present.rs @@ -4,7 +4,7 @@ use ockam::Context; use ockam_api::nodes::{BackgroundNode, Credentials}; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::util::node_rpc; use crate::CommandGlobalOpts; @@ -22,7 +22,6 @@ pub struct PresentCommand { impl PresentCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -36,8 +35,7 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: PresentCommand, ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; node.present_credential(ctx, &cmd.to, cmd.oneway).await?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/credential/show.rs b/implementations/rust/ockam/ockam_command/src/credential/show.rs index 5160f3fdd5b..27f10c452bd 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/show.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/show.rs @@ -1,15 +1,10 @@ use clap::{arg, Args}; use colorful::Colorful; use indoc::formatdoc; -use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; -use crate::credential::identities; use crate::output::CredentialAndPurposeKeyDisplay; -use crate::{ - credential::validate_encoded_cred, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, -}; +use crate::{util::node_rpc, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct ShowCommand { @@ -31,43 +26,20 @@ async fn run_impl( _ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); + let named_credential = opts + .state + .get_credential_by_name(&cmd.credential_name) + .await?; - let cred_name = &cmd.credential_name; - let cred = opts.state.credentials.get(cred_name)?; - let cred_config = cred.config(); - - let identities = identities(&vault_name, &opts).await?; - identities - .identities_creation() - .import( - Some(&cred_config.issuer_identifier), - &cred_config.encoded_issuer_change_history, - ) - .await - .into_diagnostic()?; - - let is_verified = match validate_encoded_cred( - &cred_config.encoded_credential, - identities, - &cred_config.issuer_identifier, - ) - .await - { - Ok(_) => "✔︎".light_green(), - Err(_) => "✕".light_red(), - }; - - let cred = cred_config.credential()?; + let is_verified = "✔︎".light_green(); + let credential = named_credential.credential_and_purpose_key(); let plain = formatdoc!( r#" - Credential: {cred_name} {is_verified} + Credential: {} {is_verified} {} "#, - CredentialAndPurposeKeyDisplay(cred) + &cmd.credential_name, + CredentialAndPurposeKeyDisplay(credential) ); opts.terminal.stdout().plain(plain).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/credential/store.rs b/implementations/rust/ockam/ockam_command/src/credential/store.rs index 29f5f9d786c..b82e4c3089a 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/store.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/store.rs @@ -1,16 +1,17 @@ -use crate::credential::{identities, identity}; -use crate::{ - credential::validate_encoded_cred, fmt_log, fmt_ok, terminal::OckamColor, util::node_rpc, - vault::default_vault_name, CommandGlobalOpts, -}; +use std::path::PathBuf; + use clap::Args; use colorful::Colorful; -use miette::miette; +use miette::IntoDiagnostic; +use tokio::sync::Mutex; +use tokio::try_join; + +use ockam::identity::Identity; use ockam::Context; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::{CredentialConfig, StateDirTrait}; -use std::path::PathBuf; -use tokio::{sync::Mutex, try_join}; + +use crate::credential::verify::verify_credential; +use crate::{fmt_log, fmt_ok, terminal::OckamColor, util::node_rpc, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct StoreCommand { @@ -50,57 +51,29 @@ async fn run_impl( let is_finished: Mutex = Mutex::new(false); let send_req = async { - let cred_as_str = match (&cmd.credential, &cmd.credential_path) { - (_, Some(credential_path)) => tokio::fs::read_to_string(credential_path) - .await? - .trim() - .to_string(), - (Some(credential), _) => credential.to_string(), - _ => { - *is_finished.lock().await = true; - return crate::Result::Err( - miette!("Credential or Credential Path argument must be provided").into(), - ); - } - }; - - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); - - let identities = match identities(&vault_name, &opts).await { - Ok(i) => i, - Err(_) => { - *is_finished.lock().await = true; - return Err(miette!("Invalid state").into()); - } - }; - - let issuer = match identity(&cmd.issuer, identities.clone()).await { - Ok(i) => i, - Err(_) => { - *is_finished.lock().await = true; - return Err(miette!("Issuer is invalid {}", &cmd.issuer).into()); - } - }; - - let issuer_exported = identities.export_identity(&issuer).await?; - let cred = hex::decode(&cred_as_str)?; - if let Err(e) = validate_encoded_cred(&cred, identities, &issuer).await { - *is_finished.lock().await = true; - return Err(miette!("Credential is invalid\n{}", e).into()); - } - + let issuer = verify_issuer(&opts, &cmd.issuer, &cmd.vault).await?; + let credential_and_purpose_key = verify_credential( + &opts, + issuer.identifier(), + &cmd.credential, + &cmd.credential_path, + &cmd.vault, + ) + .await?; // store - opts.state.credentials.create( - &cmd.credential_name, - CredentialConfig::new(issuer.clone(), issuer_exported, cred)?, - )?; + opts.state + .store_credential( + &cmd.credential_name, + &issuer, + credential_and_purpose_key.clone(), + ) + .await + .into_diagnostic()?; *is_finished.lock().await = true; - - Ok(cred_as_str) + Ok(credential_and_purpose_key + .encode_as_string() + .into_diagnostic()?) }; let output_messages = vec![format!("Storing credential...")]; @@ -113,7 +86,7 @@ async fn run_impl( opts.terminal .stdout() - .machine(credential.to_string()) + .machine(credential.clone()) .json(serde_json::json!( { "name": cmd.credential_name, @@ -131,3 +104,28 @@ async fn run_impl( Ok(()) } + +async fn verify_issuer( + opts: &CommandGlobalOpts, + issuer: &str, + vault: &Option, +) -> miette::Result { + let vault = opts + .state + .get_named_vault_or_default(vault) + .await? + .vault() + .await?; + let identities = opts.state.make_identities(vault).await?; + + let identifier = identities + .identities_creation() + .import(None, &hex::decode(issuer).into_diagnostic()?) + .await + .into_diagnostic()?; + let identity = identities + .get_identity(&identifier) + .await + .into_diagnostic()?; + Ok(identity) +} diff --git a/implementations/rust/ockam/ockam_command/src/credential/verify.rs b/implementations/rust/ockam/ockam_command/src/credential/verify.rs index 6df92389e26..e50d4467007 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/verify.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/verify.rs @@ -1,20 +1,17 @@ use std::path::PathBuf; +use std::sync::Arc; -use crate::{ - fmt_err, fmt_log, fmt_ok, util::node_rpc, vault::default_vault_name, CommandGlobalOpts, -}; -use miette::miette; - -use crate::credential::identities; use clap::Args; use colorful::Colorful; -use ockam::identity::Identifier; -use ockam::Context; +use miette::{miette, IntoDiagnostic}; use tokio::{sync::Mutex, try_join}; -use crate::util::parsers::identity_identifier_parser; +use ockam::identity::models::CredentialAndPurposeKey; +use ockam::identity::{Identifier, Identities}; +use ockam::Context; -use super::validate_encoded_cred; +use crate::util::parsers::identity_identifier_parser; +use crate::{fmt_err, fmt_log, fmt_ok, util::node_rpc, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] pub struct VerifyCommand { @@ -46,13 +43,46 @@ async fn run_impl( _ctx: Context, (opts, cmd): (CommandGlobalOpts, VerifyCommand), ) -> miette::Result<()> { + let (is_valid, plain_text) = match verify_credential( + &opts, + cmd.issuer(), + &cmd.credential, + &cmd.credential_path, + &cmd.vault, + ) + .await + { + Ok(_) => (true, fmt_ok!("Credential is")), + Err(e) => ( + false, + fmt_err!("Credential is not valid\n") + &fmt_log!("{}", e), + ), + }; + + opts.terminal + .stdout() + .machine(is_valid.to_string()) + .json(serde_json::json!({ "is_valid": is_valid })) + .plain(plain_text) + .write_line()?; + + Ok(()) +} + +pub async fn verify_credential( + opts: &CommandGlobalOpts, + issuer: &Identifier, + credential: &Option, + credential_path: &Option, + vault: &Option, +) -> miette::Result { opts.terminal .write_line(&fmt_log!("Verifying credential...\n"))?; let is_finished: Mutex = Mutex::new(false); let send_req = async { - let cred_as_str = match (&cmd.credential, &cmd.credential_path) { + let credential_as_str = match (&credential, &credential_path) { (_, Some(credential_path)) => tokio::fs::read_to_string(credential_path) .await? .trim() @@ -60,35 +90,29 @@ async fn run_impl( (Some(credential), _) => credential.clone(), _ => { *is_finished.lock().await = true; - return crate::Result::Err( + return Err( miette!("Credential or Credential Path argument must be provided").into(), ); } }; - let vault_name = cmd - .vault - .clone() - .unwrap_or_else(|| default_vault_name(&opts.state)); - - let issuer = cmd.issuer(); - - let identities = match identities(&vault_name, &opts).await { + let vault = opts + .state + .get_named_vault_or_default(vault) + .await? + .vault() + .await?; + let identities = match opts.state.make_identities(vault).await { Ok(i) => i, - Err(_) => { + Err(e) => { *is_finished.lock().await = true; - return Err(miette!("Invalid state").into()); + return Err(e.into()); } }; - let cred = hex::decode(&cred_as_str)?; - let is_valid = match validate_encoded_cred(&cred, identities, issuer).await { - Ok(_) => (true, String::new()), - Err(e) => (false, e.to_string()), - }; - + let result = validate_encoded_credential(identities, issuer, &credential_as_str).await; *is_finished.lock().await = true; - Ok(is_valid) + result.map_err(|e| miette!("Credential is invalid\n{}", e).into()) }; let output_messages = vec![format!("Verifying credential...")]; @@ -97,18 +121,22 @@ async fn run_impl( .terminal .progress_output(&output_messages, &is_finished); - let ((is_valid, reason), _) = try_join!(send_req, progress_output)?; - let plain_text = match is_valid { - true => fmt_ok!("Credential is valid"), - false => fmt_err!("Credential is not valid\n") + &fmt_log!("{reason}"), - }; + let (credential_and_purpose_key, _) = try_join!(send_req, progress_output)?; - opts.terminal - .stdout() - .machine(is_valid.to_string()) - .json(serde_json::json!({ "is_valid": is_valid })) - .plain(plain_text) - .write_line()?; + Ok(credential_and_purpose_key) +} - Ok(()) +async fn validate_encoded_credential( + identities: Arc, + issuer: &Identifier, + credential_as_str: &str, +) -> miette::Result { + let verification = identities.credentials().credentials_verification(); + let credential_and_purpose_key: CredentialAndPurposeKey = + minicbor::decode(&hex::decode(credential_as_str).into_diagnostic()?).into_diagnostic()?; + verification + .verify_credential(None, &[issuer.clone()], &credential_and_purpose_key) + .await + .into_diagnostic()?; + Ok(credential_and_purpose_key) } diff --git a/implementations/rust/ockam/ockam_command/src/enroll/command.rs b/implementations/rust/ockam/ockam_command/src/enroll/command.rs index 08b4c9a6a41..c46de152315 100644 --- a/implementations/rust/ockam/ockam_command/src/enroll/command.rs +++ b/implementations/rust/ockam/ockam_command/src/enroll/command.rs @@ -9,10 +9,8 @@ use tokio::sync::Mutex; use tokio::try_join; use tracing::{info, warn}; -use ockam::identity::Identifier; use ockam::Context; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::{random_name, update_enrolled_identity, SpaceConfig}; +use ockam_api::cli_state::random_name; use ockam_api::cloud::enroll::auth0::*; use ockam_api::cloud::project::{Project, Projects}; use ockam_api::cloud::space::{Space, Spaces}; @@ -22,7 +20,6 @@ use ockam_api::enroll::oidc_service::OidcService; use ockam_api::nodes::InMemoryNode; use crate::enroll::OidcServiceExt; -use crate::identity::initialize_identity_if_default; use crate::operation::util::check_for_completion; use crate::output::OutputFormat; use crate::project::util::check_project_readiness; @@ -51,7 +48,6 @@ pub struct EnrollCommand { impl EnrollCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.identity); node_rpc(rpc, (opts, self)); } } @@ -75,7 +71,7 @@ fn ctrlc_handler(opts: CommandGlobalOpts) { "\n{} Received Ctrl+C again. Cancelling {}. Please try again.", "!".red(), "ockam enroll".bold().light_yellow() ) - .as_str(), + .as_str(), ); process::exit(2); } else { @@ -84,12 +80,12 @@ fn ctrlc_handler(opts: CommandGlobalOpts) { "\n{} {} is still in progress. If you would like to stop the enrollment process, press Ctrl+C again.", "!".red(), "ockam enroll".bold().light_yellow() ) - .as_str(), + .as_str(), ); is_confirmation.store(true, Ordering::Relaxed); } }) - .expect("Error setting Ctrl-C handler"); + .expect("Error setting Ctrl-C handler"); } async fn run_impl( @@ -114,9 +110,7 @@ async fn run_impl( let user_info = oidc_service .wait_for_email_verification(&token, Some(&opts.terminal)) .await?; - opts.state - .users_info - .overwrite(&user_info.email, user_info.clone())?; + opts.state.store_user(&user_info).await?; let node = InMemoryNode::start(ctx, &opts.state).await?; let controller = node.create_controller().await?; @@ -125,7 +119,19 @@ async fn run_impl( .await .wrap_err("Failed to enroll your local identity with Ockam Orchestrator")?; - let identifier = retrieve_user_project(&opts, ctx, &node).await?; + let project = retrieve_user_project(&opts, ctx, &node).await?; + let identifier = node.identifier(); + opts.state + .set_identifier_as_enrolled(&identifier) + .await + .wrap_err(format!( + "Unable to set the local identity as enrolled with project {}", + project + .name + .to_string() + .color(OckamColor::PrimaryResource.color()) + ))?; + info!("Enrolled a user with the Identifier {}", identifier); opts.terminal.write_line(&fmt_ok!( "Enrolled {} as one of the Ockam identities of your Orchestrator account {}.", @@ -141,13 +147,18 @@ pub async fn retrieve_user_project( opts: &CommandGlobalOpts, ctx: &Context, node: &InMemoryNode, -) -> Result { - let space = default_space(opts, ctx, &node.create_controller().await?) +) -> Result { + // return the default project if there is one already stored locally + if let Ok(project) = opts.state.get_default_project().await { + return Ok(project); + }; + + let space = get_user_space(opts, ctx, node) .await .wrap_err("Unable to retrieve and set a space as default")?; info!("Retrieved the user default space {:?}", space); - let project = default_project(opts, ctx, node, &space) + let project = get_user_project(opts, ctx, node, &space) .await .wrap_err(format!( "Unable to retrieve and set a project as default with space {}", @@ -157,19 +168,7 @@ pub async fn retrieve_user_project( .color(OckamColor::PrimaryResource.color()) ))?; info!("Retrieved the user default project {:?}", project); - - let identifier = update_enrolled_identity(&opts.state, &node.node_name()) - .await - .wrap_err(format!( - "Unable to set the local identity as enrolled with project {}", - project - .name - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; - info!("Enrolled a user with the Identifier {}", identifier); - - Ok(identifier) + Ok(project) } /// Enroll a user with a token, using the controller @@ -188,17 +187,23 @@ pub async fn enroll_with_node( Ok(()) } -async fn default_space( +async fn get_user_space( opts: &CommandGlobalOpts, ctx: &Context, - controller: &Controller, + node: &InMemoryNode, ) -> Result { - // Get available spaces for node's identity + // return the default space if there is one already stored locally + if let Ok(space) = opts.state.get_default_space().await { + return Ok(space); + }; + + // Otherwise get the available spaces for node's identity + // Those spaces might have been created previously and all the local state reset opts.terminal .write_line(&fmt_log!("Getting available spaces in your account..."))?; let is_finished = Mutex::new(false); let get_spaces = async { - let spaces: Vec = controller.list_spaces(ctx).await?; + let spaces = node.get_spaces(ctx).await?; *is_finished.lock().await = true; Ok(spaces) }; @@ -206,79 +211,64 @@ async fn default_space( let message = vec![format!("Checking for any existing spaces...")]; let progress_output = opts.terminal.progress_output(&message, &is_finished); - let (mut available_spaces, _) = try_join!(get_spaces, progress_output)?; + let (spaces, _) = try_join!(get_spaces, progress_output)?; // If the identity has no spaces, create one - let default_space = if available_spaces.is_empty() { - opts.terminal - .write_line(&fmt_para!("No spaces are defined in your account."))? - .write_line(&fmt_para!( - "Creating a trial space for you ({}) ...", - "everything in it will be deleted in 15 days" - .to_string() - .color(OckamColor::FmtWARNBackground.color()) - ))? - .write_line(&fmt_para!( - "To learn more about production ready spaces in Ockam Orchestrator, contact us at: {}", - "hello@ockam.io".to_string().color(OckamColor::PrimaryResource.color()) - ))?; - - let is_finished = Mutex::new(false); - let name = random_name(); - let space_name = name.clone(); - let create_space = async { - let space = controller.create_space(ctx, space_name, vec![]).await?; - *is_finished.lock().await = true; - Ok(space) - }; - - let message = vec![format!( - "Creating space {}...", - name.color(OckamColor::PrimaryResource.color()) - )]; - let progress_output = opts.terminal.progress_output(&message, &is_finished); - let (space, _) = try_join!(create_space, progress_output)?; - space - } - // If it has, return the first one on the list - else { - for space in &available_spaces { - opts.state - .spaces - .overwrite(&space.name, SpaceConfig::from(space))?; - } - - let space = available_spaces - .drain(..1) - .next() - .expect("already checked that is not empty"); - - opts.terminal.write_line(&fmt_log!( - "Found space {}.", + let space = match spaces.first() { + None => { + opts.terminal + .write_line(&fmt_para!("No spaces are defined in your account."))? + .write_line(&fmt_para!( + "Creating a trial space for you ({}) ...", + "everything in it will be deleted in 15 days" + .to_string() + .color(OckamColor::FmtWARNBackground.color()) + ))? + .write_line(&fmt_para!( + "To learn more about production ready spaces in Ockam Orchestrator, contact us at: {}", + "hello@ockam.io".to_string().color(OckamColor::PrimaryResource.color())))?; + + let is_finished = Mutex::new(false); + let space_name = random_name(); + let create_space = async { + let space = node.create_space(ctx, &space_name, vec![]).await?; + *is_finished.lock().await = true; + Ok(space) + }; + + let message = vec![format!( + "Creating space {}...", + space_name + .clone() + .color(OckamColor::PrimaryResource.color()) + )]; + let progress_output = opts.terminal.progress_output(&message, &is_finished); + let (space, _) = try_join!(create_space, progress_output)?; space - .name - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; - space + } + Some(space) => { + opts.terminal.write_line(&fmt_log!( + "Found space {}.", + space + .name + .clone() + .color(OckamColor::PrimaryResource.color()) + ))?; + space.clone() + } }; - opts.state - .spaces - .overwrite(&default_space.name, SpaceConfig::from(&default_space))?; opts.terminal.write_line(&fmt_ok!( "Marked this space as your default space, on this machine.\n" ))?; - Ok(default_space) + Ok(space) } -async fn default_project( +async fn get_user_project( opts: &CommandGlobalOpts, ctx: &Context, node: &InMemoryNode, space: &Space, ) -> Result { - let controller = node.create_controller().await?; - // Get available project for the given space opts.terminal.write_line(&fmt_log!( "Getting available projects in space {}...", @@ -290,7 +280,7 @@ async fn default_project( let is_finished = Mutex::new(false); let get_projects = async { - let projects = controller.list_projects(ctx).await?; + let projects = node.get_projects(ctx).await?; *is_finished.lock().await = true; Ok(projects) }; @@ -298,85 +288,68 @@ async fn default_project( let message = vec![format!("Checking for any existing projects...")]; let progress_output = opts.terminal.progress_output(&message, &is_finished); - let (mut available_projects, _) = try_join!(get_projects, progress_output)?; + let (projects, _) = try_join!(get_projects, progress_output)?; // If the space has no projects, create one - let default_project = if available_projects.is_empty() { - opts.terminal - .write_line(&fmt_para!( - "No projects are defined in the space {}.", - space - .name + let project = match projects.first() { + None => { + opts.terminal + .write_line(&fmt_para!( + "No projects are defined in the space {}.", + space + .name + .to_string() + .color(OckamColor::PrimaryResource.color()) + ))? + .write_line(&fmt_para!("Creating a project for you..."))?; + + let is_finished = Mutex::new(false); + let project_name = "default".to_string(); + let get_project = async { + let project = node + .create_project(ctx, &space.id, &project_name, vec![]) + .await?; + *is_finished.lock().await = true; + Ok(project) + }; + + let message = vec![format!( + "Creating project {}...", + project_name .to_string() .color(OckamColor::PrimaryResource.color()) - ))? - .write_line(&fmt_para!("Creating a project for you..."))?; - - let is_finished = Mutex::new(false); - let project_name = "default".to_string(); - let get_project = async { - let project = controller - .create_project(ctx, space.id.clone(), project_name.clone(), vec![]) - .await?; - *is_finished.lock().await = true; - Ok(project) - }; + )]; + let progress_output = opts.terminal.progress_output(&message, &is_finished); + let (project, _) = try_join!(get_project, progress_output)?; - let message = vec![format!( - "Creating project {}...", - project_name - .to_string() - .color(OckamColor::PrimaryResource.color()) - )]; - let progress_output = opts.terminal.progress_output(&message, &is_finished); - let (project, _) = try_join!(get_project, progress_output)?; - - opts.terminal.write_line(&fmt_ok!( - "Created project {}.", - project_name - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; + opts.terminal.write_line(&fmt_ok!( + "Created project {}.", + project_name + .to_string() + .color(OckamColor::PrimaryResource.color()) + ))?; - let operation_id = project.operation_id.clone().unwrap(); - check_for_completion(opts, ctx, &controller, &operation_id).await?; + let operation_id = project.operation_id.clone().unwrap(); + check_for_completion(opts, ctx, &node.create_controller().await?, &operation_id) + .await?; - project.to_owned() - } - // If it has, return the "default" project or first one on the list - else { - for project in &available_projects { - opts.state - .projects - .overwrite(&project.name, project.clone())?; + project.to_owned() + } + Some(project) => { + opts.terminal.write_line(&fmt_log!( + "Found project {}.", + project + .project_name() + .color(OckamColor::PrimaryResource.color()) + ))?; + project.clone() } - let p = match available_projects.iter().find(|ns| ns.name == "default") { - None => available_projects - .drain(..1) - .next() - .expect("already checked that is not empty"), - Some(p) => p.to_owned(), - }; - opts.terminal.write_line(&fmt_log!( - "Found project {}.", - p.name - .to_string() - .color(OckamColor::PrimaryResource.color()) - ))?; - p }; - let project = check_project_readiness(opts, ctx, node, default_project).await?; + check_project_readiness(opts, ctx, node, project.clone()).await?; opts.terminal.write_line(&fmt_ok!( "Marked this project as your default project, on this machine.\n" ))?; - - opts.state - .projects - .overwrite(&project.name, project.clone())?; - opts.state - .trust_contexts - .overwrite(&project.name, project.clone().try_into()?)?; Ok(project) } diff --git a/implementations/rust/ockam/ockam_command/src/flow_control/add_consumer.rs b/implementations/rust/ockam/ockam_command/src/flow_control/add_consumer.rs index bd155e463aa..8d5c21cf5d9 100644 --- a/implementations/rust/ockam/ockam_command/src/flow_control/add_consumer.rs +++ b/implementations/rust/ockam/ockam_command/src/flow_control/add_consumer.rs @@ -1,12 +1,11 @@ use clap::Args; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::nodes::BackgroundNode; use ockam_core::flow_control::FlowControlId; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, NodeOpts}; +use crate::node::NodeOpts; use crate::util::{api, node_rpc}; use crate::CommandGlobalOpts; @@ -41,9 +40,7 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: AddConsumerCommand, ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; node.tell(ctx, api::add_consumer(cmd.flow_control_id, cmd.address)) .await?; diff --git a/implementations/rust/ockam/ockam_command/src/identity/create.rs b/implementations/rust/ockam/ockam_command/src/identity/create.rs index df41cf9bebd..f7e5e3f5995 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/create.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/create.rs @@ -1,16 +1,15 @@ -use crate::terminal::OckamColor; -use crate::util::node_rpc; -use crate::{docs, fmt_log, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use miette::miette; +use tokio::sync::Mutex; +use tokio::try_join; + use ockam::identity::Identifier; use ockam::Context; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_vault::{HandleToSecret, SigningSecretKeyHandle}; -use tokio::sync::Mutex; -use tokio::try_join; + +use crate::terminal::OckamColor; +use crate::util::node_rpc; +use crate::{docs, fmt_log, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -66,56 +65,37 @@ impl CreateCommand { let is_finished: Mutex = Mutex::new(false); let send_req = async { - let default_vault_created = - self.vault.is_none() && opts.state.vaults.default().is_err(); - let vault_state = opts.state.create_vault_state(self.vault.as_deref()).await?; - if default_vault_created { + let existing_vaults = opts.state.get_named_vaults().await?.len(); + + let vault = match &self.vault { + Some(vault_name) => opts.state.create_named_vault(vault_name).await?, + None => opts.state.get_default_named_vault().await?, + }; + let updated_vaults = opts.state.get_named_vaults().await?.len(); + + // If a new vault has been created display a message + if updated_vaults > existing_vaults { opts.terminal.write_line(&fmt_log!( "Default vault created: {}\n", - &vault_state - .name() - .to_string() - .color(OckamColor::PrimaryResource.color()) + vault.name().color(OckamColor::PrimaryResource.color()) ))?; - } - - let vault = vault_state.get().await?; - - let identities_creation = opts - .state - .get_identities(vault) - .await? - .identities_creation(); + }; - // Create an identity using the KMS key, if provided. - let identifier = match &self.key_id { + let identity = match &self.key_id { Some(key_id) => { - if !vault_state.config().is_aws() { - Err(miette!( - "Vault {} is not an AWS KMS vault", - self.vault.clone().unwrap_or("default".to_string()), - )) - } else { - let handle = SigningSecretKeyHandle::ECDSASHA256CurveP256( - HandleToSecret::new(key_id.as_bytes().to_vec()), - ); - - Ok(identities_creation - .identity_builder() - .with_existing_key(handle) - .build() - .await?) - } + opts.state + .create_identity_with_key_id(&self.name, &vault.name(), key_id.as_ref()) + .await? } - None => Ok(identities_creation.create_identity().await?), - }?; - - opts.state - .create_identity_state(&identifier, Some(&self.name)) - .await?; + None => { + opts.state + .create_identity_with_name_and_vault(&self.name, &vault.name()) + .await? + } + }; *is_finished.lock().await = true; - Ok(identifier) + Ok(identity.identifier()) }; let output_messages = vec![format!("Creating identity...")]; diff --git a/implementations/rust/ockam/ockam_command/src/identity/default.rs b/implementations/rust/ockam/ockam_command/src/identity/default.rs index 9f79a0f8d94..666d9ee18dc 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/default.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/default.rs @@ -1,9 +1,11 @@ -use crate::util::local_cmd; -use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::traits::StateDirTrait; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/default/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt"); @@ -21,42 +23,41 @@ pub struct DefaultCommand { impl DefaultCommand { pub fn run(self, options: CommandGlobalOpts) { - local_cmd(run_impl(options, self)); + node_rpc(run_impl, (options, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DefaultCommand) -> miette::Result<()> { - if let Some(name) = cmd.name { - let state = opts.state.identities; - let idt = state.get(&name)?; - // If it's already the default, warn the user and exit - if state.is_default(idt.name())? { - Err(miette!( - "The identity named '{}' is already the default", - &name - )) +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, DefaultCommand), +) -> miette::Result<()> { + match cmd.name { + Some(name) => { + if opts.state.is_default_identity_by_name(&name).await? { + Err(miette!( + "The identity named '{}' is already the default", + &name + ))? + } else { + opts.state.set_as_default_identity(&name).await?; + opts.terminal + .stdout() + .plain(fmt_ok!("The identity named '{}' is now the default", &name)) + .machine(&name) + .write_line()?; + } } - // Otherwise, set it as default - else { - state.set_default(idt.name())?; + None => { + let identity = opts.state.get_default_named_identity().await?; opts.terminal .stdout() - .plain(fmt_ok!("The identity named '{}' is now the default", &name)) - .machine(&name) + .plain(fmt_ok!( + "The name of the default identity is '{}'", + identity.name() + )) .write_line()?; - Ok(()) } - } - // No argument provided, show default identity name - else { - let state = opts.state.identities.get_or_default(None)?; - opts.terminal - .stdout() - .plain(fmt_ok!( - "The name of the default identity is '{}'", - state.name() - )) - .write_line()?; - Ok(()) - } + }; + + Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/identity/delete.rs b/implementations/rust/ockam/ockam_command/src/identity/delete.rs index af587c5faf7..b372d6b6dd7 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/delete.rs @@ -1,14 +1,12 @@ -use crate::terminal::tui::DeleteCommandTui; -use crate::util::node_rpc; -use crate::{docs, fmt_ok, fmt_warn, CommandGlobalOpts, Terminal, TerminalStream}; use clap::Args; use colorful::Colorful; - use console::Term; + use ockam::Context; -use ockam_api::cli_state::traits::StateDirTrait; -use super::get_identity_name; +use crate::terminal::tui::DeleteCommandTui; +use crate::util::node_rpc; +use crate::{docs, fmt_ok, fmt_warn, CommandGlobalOpts, Terminal, TerminalStream}; const LONG_ABOUT: &str = include_str!("./static/delete/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -76,17 +74,28 @@ impl DeleteCommandTui for DeleteTui { } async fn get_arg_item_name_or_default(&self) -> miette::Result { - Ok(get_identity_name(&self.opts.state, &self.cmd.name)) + Ok(self + .opts + .state + .get_named_identity_or_default(&self.cmd.name) + .await? + .name()) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.identities.list_items_names()?) + Ok(self + .opts + .state + .get_named_identities() + .await? + .iter() + .map(|i| i.name()) + .collect()) } async fn delete_single(&self, item_name: &str) -> miette::Result<()> { let state = &self.opts.state; - let idt = state.identities.get(item_name)?; - state.delete_identity(idt)?; + state.delete_identity_by_name(item_name).await?; self.terminal() .stdout() .plain(fmt_ok!( @@ -100,17 +109,20 @@ impl DeleteCommandTui for DeleteTui { } async fn delete_multiple(&self, selected_items_names: Vec) -> miette::Result<()> { - let plain = selected_items_names - .iter() - .map(|name| { - let idt = self.opts.state.identities.get(name)?; - if self.opts.state.delete_identity(idt).is_ok() { - Ok(fmt_ok!("Identity '{name}' deleted\n")) - } else { - Ok(fmt_warn!("Failed to delete identity '{name}'\n")) - } - }) - .collect::>()?; + let mut plain = String::new(); + for name in selected_items_names { + if self + .opts + .state + .delete_identity_by_name(name.as_ref()) + .await + .is_ok() + { + plain.push_str(&fmt_ok!("Identity '{name}' deleted\n")) + } else { + plain.push_str(&fmt_warn!("Failed to delete identity '{name}'\n")) + } + } self.terminal().stdout().plain(plain).write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/identity/list.rs b/implementations/rust/ockam/ockam_command/src/identity/list.rs index 8856ab41dbe..ea0950755f5 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/list.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/list.rs @@ -6,14 +6,10 @@ use crate::{docs, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::traits::StateDirTrait; - use ockam_node::Context; use serde::Serialize; use serde_json::json; use std::fmt::Write; -use tokio::sync::Mutex; -use tokio::try_join; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -38,37 +34,20 @@ impl ListCommand { options: (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { let (opts, _cmd) = options; - let mut identities: Vec = Vec::new(); - - let idts = opts.state.identities.list()?; - for identity in idts.iter() { - let is_finished: Mutex = Mutex::new(false); - - let send_req = async { - let i = IdentityListOutput::new( - identity.name().to_string(), - identity.identifier().to_string(), - opts.state.identities.default()?.name() == identity.name(), - ); - *is_finished.lock().await = true; - Ok(i) - }; - - let output_messages = vec![format!( - "Retrieving identity {}...\n", - &identity.name().color(OckamColor::PrimaryResource.color()) - )]; - - let progress_output = opts - .terminal - .progress_output(&output_messages, &is_finished); - - let (identity_states, _) = try_join!(send_req, progress_output)?; - identities.push(identity_states); + let mut identities_list: Vec = Vec::new(); + + let identities = opts.state.get_named_identities().await?; + for identity in identities.iter() { + let identity_output = IdentityListOutput::new( + identity.name(), + identity.identifier().to_string(), + identity.is_default(), + ); + identities_list.push(identity_output); } let list = opts.terminal.build_list( - &identities, + &identities_list, "Identities", "No identities found on this system.", )?; diff --git a/implementations/rust/ockam/ockam_command/src/identity/mod.rs b/implementations/rust/ockam/ockam_command/src/identity/mod.rs index 5c038bd2561..8937b8daef8 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/mod.rs @@ -1,8 +1,4 @@ -mod create; -mod default; -mod delete; -mod list; -mod show; +use clap::{Args, Subcommand}; pub use create::CreateCommand; pub(crate) use delete::DeleteCommand; @@ -11,9 +7,12 @@ pub(crate) use show::ShowCommand; use crate::identity::default::DefaultCommand; use crate::{docs, CommandGlobalOpts}; -use clap::{Args, Subcommand}; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::CliState; + +mod create; +mod default; +mod delete; +mod list; +mod show; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); @@ -49,65 +48,3 @@ impl IdentityCommand { } } } - -/// If the required identity is the default identity but if it has not been initialized yet -/// then initialize it -pub fn initialize_identity_if_default(opts: &CommandGlobalOpts, name: &Option) { - let name = get_identity_name(&opts.state, name); - if name == "default" && opts.state.identities.default().is_err() { - create_default_identity(opts); - } -} - -/// Return the name if identity_name is Some otherwise return the name of the default identity -pub fn get_identity_name(cli_state: &CliState, identity_name: &Option) -> String { - identity_name - .clone() - .unwrap_or_else(|| get_default_identity_name(cli_state)) -} - -/// Return the name of the default identity -pub fn get_default_identity_name(cli_state: &CliState) -> String { - cli_state - .identities - .default() - .map(|i| i.name().to_string()) - .unwrap_or_else(|_| "default".to_string()) -} - -/// Create the default identity -pub fn create_default_identity(opts: &CommandGlobalOpts) { - let default = "default"; - let create_command = CreateCommand::new(default.into(), None, None); - create_command.run(opts.clone().set_quiet()); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::GlobalArgs; - use ockam_api::cli_state::StateItemTrait; - - #[test] - fn test_initialize() { - let state = CliState::test().unwrap(); - let opts = CommandGlobalOpts::new_for_test(GlobalArgs::default(), state); - - // on start-up there is no default identity - assert!(opts.state.identities.default().is_err()); - - // if no name is given then the default identity is initialized - initialize_identity_if_default(&opts, &None); - assert!(opts.state.identities.default().is_ok()); - - // if "default" is given as a name the default identity is initialized - opts.state.identities.default().unwrap().delete().unwrap(); - initialize_identity_if_default(&opts, &Some("default".into())); - assert!(opts.state.identities.default().is_ok()); - - // if the name of another identity is given then the default identity is not initialized - opts.state.identities.default().unwrap().delete().unwrap(); - initialize_identity_if_default(&opts, &Some("other".into())); - assert!(opts.state.identities.default().is_err()); - } -} diff --git a/implementations/rust/ockam/ockam_command/src/identity/show.rs b/implementations/rust/ockam/ockam_command/src/identity/show.rs index 66dfcf1ffbc..e6be9c5cac7 100644 --- a/implementations/rust/ockam/ockam_command/src/identity/show.rs +++ b/implementations/rust/ockam/ockam_command/src/identity/show.rs @@ -1,6 +1,5 @@ use std::fmt::Display; -use crate::identity::get_identity_name; use crate::identity::list::IdentityListOutput; use crate::output::{EncodeFormat, IdentifierDisplay, Output, VerifyingPublicKeyDisplay}; use crate::util::node_rpc; @@ -8,8 +7,8 @@ use crate::{docs, CommandGlobalOpts}; use clap::Args; use miette::IntoDiagnostic; use ockam::identity::verified_change::VerifiedChange; -use ockam::identity::{Identifier, Identity, Vault}; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; +use ockam::identity::{Identifier, Identity}; +use ockam_api::NamedIdentity; use ockam_node::Context; use serde::Serialize; use serde_json::{json, to_string_pretty}; @@ -21,9 +20,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show the details of an identity #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ShowCommand { #[arg()] @@ -53,26 +52,32 @@ impl ShowCommand { let (opts, cmd) = options; if cmd.name.is_some() || !opts.terminal.can_ask_for_user_input() { - let name = get_identity_name(&opts.state, &cmd.name); - Self::show_single_identity(&opts, &name, cmd.full, cmd.encoding).await?; + Self::show_single_identity(&opts, &cmd.name, cmd.full, cmd.encoding).await?; return Ok(()); } - let id_names: Vec = opts.state.identities.list_items_names()?; - match id_names.len() { + let identities: Vec = opts.state.get_named_identities().await?; + let identities_names: Vec = identities.iter().map(|i| i.name()).collect(); + match identities_names.len() { 0 => { opts.terminal .stdout() - .plain("There are no nodes to show") + .plain("There are no identities to show") .write_line()?; } 1 => { - Self::show_single_identity(&opts, &id_names[0], cmd.full, cmd.encoding).await?; + Self::show_single_identity( + &opts, + &identities_names.first().cloned(), + cmd.full, + cmd.encoding, + ) + .await?; } _ => { let selected_names = opts.terminal.select_multiple( "Select one or more identities that you want to show".to_string(), - id_names, + identities_names, ); if selected_names.is_empty() { @@ -97,40 +102,25 @@ impl ShowCommand { async fn show_single_identity( opts: &CommandGlobalOpts, - name: &str, + name: &Option, full: bool, encoding: Option, ) -> miette::Result<()> { - let state = opts.state.identities.get(name)?; - let identifier = state.config().identifier(); + let identity = opts.state.get_identity_by_optional_name(name).await?; + let (plain, json) = if full { - let change_history = opts - .state - .identities - .identities_repository() - .await? - .get_identity(&identifier) - .await - .into_diagnostic()?; + let change_history = identity.change_history(); if Some(EncodeFormat::Hex) == encoding { let encoded = hex::encode(change_history.export().into_diagnostic()?); let json = to_string_pretty(&json!({"encoded": &encoded})); (encoded, json) } else { - let identity: ShowIdentity = Identity::import_from_change_history( - Some(&identifier), - change_history, - Vault::create_verifying_vault(), - ) - .await - .into_diagnostic()? - .into(); - + let identity: ShowIdentity = identity.into(); (identity.to_string(), to_string_pretty(&identity)) } } else { - let identifier_display = IdentifierDisplay(identifier); + let identifier_display = IdentifierDisplay(identity.identifier().clone()); ( identifier_display.to_string(), to_string_pretty(&json!({"identifier": &identifier_display})), @@ -154,11 +144,13 @@ impl ShowCommand { let mut identities: Vec = Vec::new(); for name in selected_names { - let state = opts.state.identities.get(&name)?; - let identifier = state.config().identifier().to_string(); - let is_default = opts.state.identities.is_default(&name)?; - let identity = IdentityListOutput::new(name, identifier, is_default); - identities.push(identity); + let identity = opts.state.get_named_identity(&name).await?; + let identity_list_output = IdentityListOutput::new( + identity.name(), + identity.identifier().to_string(), + identity.is_default(), + ); + identities.push(identity_list_output); } let list = opts.terminal.build_list( diff --git a/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs index f027eb5d604..efd767682fe 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/consumer/create.rs @@ -6,7 +6,6 @@ use ockam_api::port_range::PortRange; use ockam_multiaddr::MultiAddr; use crate::kafka::util::{rpc, ArgOpts}; -use crate::node::initialize_node_if_default; use crate::{ kafka::{ kafka_consumer_default_addr, kafka_default_consumer_port_range, @@ -41,7 +40,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); let arg_opts = ArgOpts { endpoint: "/node/services/kafka_consumer".to_string(), kafka_entity: "KafkaConsumer".to_string(), diff --git a/implementations/rust/ockam/ockam_command/src/kafka/consumer/delete.rs b/implementations/rust/ockam/ockam_command/src/kafka/consumer/delete.rs index bdcfe33963a..c8114ecac2f 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/consumer/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/consumer/delete.rs @@ -5,8 +5,7 @@ use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -24,7 +23,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -33,10 +31,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = Request::delete("/node/services/kafka_consumer").body( models::services::DeleteServiceRequest::new(cmd.address.clone()), ); diff --git a/implementations/rust/ockam/ockam_command/src/kafka/consumer/list.rs b/implementations/rust/ockam/ockam_command/src/kafka/consumer/list.rs index 96846d56f81..6168742fa2c 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/consumer/list.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/consumer/list.rs @@ -1,16 +1,14 @@ use clap::Args; use colorful::Colorful; -use miette::miette; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::services::ServiceList; +use ockam_api::nodes::service::default_address::DefaultAddress; use ockam_api::nodes::BackgroundNode; -use ockam_api::DefaultAddress; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::node_rpc; use crate::{docs, fmt_err, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); @@ -19,8 +17,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Kafka Consumers #[derive(Args, Clone, Debug)] #[command( - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand { #[command(flatten)] @@ -29,7 +27,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -38,14 +35,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let services: ServiceList = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/kafka/direct/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/direct/create.rs index 979e362ee13..25974bd26a5 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/direct/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/direct/create.rs @@ -1,7 +1,6 @@ use std::net::SocketAddr; use crate::kafka::direct::rpc::{start, ArgOpts}; -use crate::node::initialize_node_if_default; use crate::{ kafka::{ kafka_default_consumer_port_range, kafka_default_consumer_server, @@ -41,7 +40,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); let arg_opts = ArgOpts { endpoint: "/node/services/kafka_direct".to_string(), kafka_entity: "KafkaDirect".to_string(), diff --git a/implementations/rust/ockam/ockam_command/src/kafka/direct/delete.rs b/implementations/rust/ockam/ockam_command/src/kafka/direct/delete.rs index d89136e8bac..771e3008210 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/direct/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/direct/delete.rs @@ -5,8 +5,7 @@ use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -24,7 +23,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -33,10 +31,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = Request::delete("/node/services/kafka_direct").body( models::services::DeleteServiceRequest::new(cmd.address.clone()), ); diff --git a/implementations/rust/ockam/ockam_command/src/kafka/direct/list.rs b/implementations/rust/ockam/ockam_command/src/kafka/direct/list.rs index e16fb51bd0a..d02f410eda3 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/direct/list.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/direct/list.rs @@ -1,16 +1,14 @@ use clap::Args; use colorful::Colorful; -use miette::miette; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::services::ServiceList; +use ockam_api::nodes::service::default_address::DefaultAddress; use ockam_api::nodes::BackgroundNode; -use ockam_api::DefaultAddress; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::node_rpc; use crate::{docs, fmt_err, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); @@ -19,8 +17,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Kafka Consumers #[derive(Args, Clone, Debug)] #[command( - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand { #[command(flatten)] @@ -29,7 +27,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -38,14 +35,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let services: ServiceList = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/kafka/direct/rpc.rs b/implementations/rust/ockam/ockam_command/src/kafka/direct/rpc.rs index 758e9335f5a..91915dc786d 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/direct/rpc.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/direct/rpc.rs @@ -10,7 +10,7 @@ use ockam_api::port_range::PortRange; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, NodeOpts}; +use crate::node::NodeOpts; use crate::service::start::start_service_impl; use crate::terminal::OckamColor; use crate::util::process_nodes_multiaddr; @@ -45,15 +45,14 @@ pub async fn start(ctx: Context, (opts, args): (CommandGlobalOpts, ArgOpts)) -> display_parse_logs(&opts); let consumer_route = if let Some(consumer_route) = consumer_route { - Some(process_nodes_multiaddr(&consumer_route, &opts.state)?) + Some(process_nodes_multiaddr(&consumer_route, &opts.state).await?) } else { None }; let is_finished = Mutex::new(false); let send_req = async { - let node_name = get_node_name(&opts.state, &node_opts.at_node); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &node_opts.at_node).await?; let payload = StartKafkaDirectRequest::new( bind_address.to_owned(), diff --git a/implementations/rust/ockam/ockam_command/src/kafka/mod.rs b/implementations/rust/ockam/ockam_command/src/kafka/mod.rs index f4bd4c6d42e..961e28793f7 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/mod.rs @@ -1,7 +1,7 @@ -use std::{net::SocketAddr, str::FromStr}; - -use ockam_api::{port_range::PortRange, DefaultAddress}; +use ockam_api::nodes::service::default_address::DefaultAddress; +use ockam_api::port_range::PortRange; use ockam_multiaddr::MultiAddr; +use std::{net::SocketAddr, str::FromStr}; pub(crate) mod consumer; pub(crate) mod direct; diff --git a/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs index 6dc9c83bcc7..ab6e616ab11 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/outlet/create.rs @@ -10,7 +10,6 @@ use ockam_api::nodes::models::services::StartServiceRequest; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::{ fmt_log, fmt_ok, kafka::{kafka_default_outlet_addr, kafka_default_outlet_server}, @@ -53,8 +52,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m let payload = StartKafkaOutletRequest::new(bootstrap_server); let payload = StartServiceRequest::new(payload, &addr); let req = Request::post("/node/services/kafka_outlet").body(payload); - let node_name = get_node_name(&opts.state, &node_opts.at_node); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &node_opts.at_node).await?; start_service_impl(&ctx, &node, "KafkaOutlet", req).await?; *is_finished.lock().await = true; diff --git a/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs b/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs index 9d8b19e3891..a17b6cc673d 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/producer/create.rs @@ -6,7 +6,6 @@ use ockam_api::port_range::PortRange; use ockam_multiaddr::MultiAddr; use crate::kafka::util::{rpc, ArgOpts}; -use crate::node::initialize_node_if_default; use crate::{ kafka::{ kafka_default_producer_port_range, kafka_default_producer_server, @@ -41,7 +40,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); let arg_opts = ArgOpts { endpoint: "/node/services/kafka_producer".to_string(), kafka_entity: "KafkaProducer".to_string(), diff --git a/implementations/rust/ockam/ockam_command/src/kafka/producer/delete.rs b/implementations/rust/ockam/ockam_command/src/kafka/producer/delete.rs index ad677238342..53e81172d6a 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/producer/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/producer/delete.rs @@ -5,8 +5,7 @@ use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -24,7 +23,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -33,10 +31,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = Request::delete("/node/services/kafka_producer").body( models::services::DeleteServiceRequest::new(cmd.address.clone()), ); diff --git a/implementations/rust/ockam/ockam_command/src/kafka/producer/list.rs b/implementations/rust/ockam/ockam_command/src/kafka/producer/list.rs index 4f9d3ecc8bf..cdee7891c2d 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/producer/list.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/producer/list.rs @@ -1,16 +1,14 @@ use clap::Args; use colorful::Colorful; -use miette::miette; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::services::ServiceList; +use ockam_api::nodes::service::default_address::DefaultAddress; use ockam_api::nodes::BackgroundNode; -use ockam_api::DefaultAddress; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::node_rpc; use crate::{docs, fmt_err, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); @@ -19,8 +17,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Kafka Producers #[derive(Args, Clone, Debug)] #[command( - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand { #[command(flatten)] @@ -29,7 +27,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -38,14 +35,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let services: ServiceList = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/kafka/util.rs b/implementations/rust/ockam/ockam_command/src/kafka/util.rs index 9809febc467..d74a90f97c7 100644 --- a/implementations/rust/ockam/ockam_command/src/kafka/util.rs +++ b/implementations/rust/ockam/ockam_command/src/kafka/util.rs @@ -10,7 +10,7 @@ use ockam_api::port_range::PortRange; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, NodeOpts}; +use crate::node::NodeOpts; use crate::service::start::start_service_impl; use crate::terminal::OckamColor; use crate::util::process_nodes_multiaddr; @@ -42,12 +42,11 @@ pub async fn rpc(ctx: Context, (opts, args): (CommandGlobalOpts, ArgOpts)) -> mi display_parse_logs(&opts); - let project_route = process_nodes_multiaddr(&project_route, &opts.state)?; + let project_route = process_nodes_multiaddr(&project_route, &opts.state).await?; let is_finished = Mutex::new(false); let send_req = async { - let node_name = get_node_name(&opts.state, &node_opts.at_node); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &node_opts.at_node).await?; let payload = StartKafkaProducerRequest::new( bootstrap_server.to_owned(), diff --git a/implementations/rust/ockam/ockam_command/src/lease/create.rs b/implementations/rust/ockam/ockam_command/src/lease/create.rs index b1c898d83de..8ee47bc6e0c 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/create.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/create.rs @@ -9,7 +9,6 @@ use time::PrimitiveDateTime; use tokio::sync::Mutex; use tokio::try_join; -use crate::identity::initialize_identity_if_default; use crate::lease::authenticate; use crate::terminal::OckamColor; use crate::util::api::{CloudOpts, TrustContextOpts}; @@ -26,7 +25,6 @@ pub struct CreateCommand {} impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts, cloud_opts: CloudOpts, trust_opts: TrustContextOpts) { - initialize_identity_if_default(&opts, &cloud_opts.identity); node_rpc(run_impl, (opts, cloud_opts, trust_opts)); } } diff --git a/implementations/rust/ockam/ockam_command/src/lease/list.rs b/implementations/rust/ockam/ockam_command/src/lease/list.rs index ba7dd501b5f..1e7ce001086 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/list.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/list.rs @@ -11,7 +11,6 @@ use time::PrimitiveDateTime; use tokio::sync::Mutex; use tokio::try_join; -use crate::identity::initialize_identity_if_default; use crate::lease::authenticate; use crate::output::Output; use crate::terminal::OckamColor; @@ -28,7 +27,6 @@ pub struct ListCommand; impl ListCommand { pub fn run(self, opts: CommandGlobalOpts, cloud_opts: CloudOpts, trust_opts: TrustContextOpts) { - initialize_identity_if_default(&opts, &cloud_opts.identity); node_rpc(run_impl, (opts, cloud_opts, trust_opts)); } } diff --git a/implementations/rust/ockam/ockam_command/src/lease/mod.rs b/implementations/rust/ockam/ockam_command/src/lease/mod.rs index 4a6e026e866..39883565330 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/mod.rs @@ -1,28 +1,24 @@ -mod create; -mod list; -mod revoke; -mod show; +use clap::{Args, Subcommand}; +use miette::IntoDiagnostic; pub use create::CreateCommand; pub use list::ListCommand; -pub use show::ShowCommand; - -use clap::{Args, Subcommand}; -use miette::{miette, Context, IntoDiagnostic}; - -use ockam_api::cli_state::{ProjectConfigCompact, StateDirTrait, StateItemTrait}; +use ockam_api::cloud::project::Projects; use ockam_api::cloud::ProjectNode; -use ockam_api::config::lookup::ProjectLookup; use ockam_api::nodes::Credentials; use ockam_api::nodes::InMemoryNode; - -use crate::identity::get_identity_name; +pub use show::ShowCommand; use crate::util::api::{CloudOpts, TrustContextOpts}; use crate::CommandGlobalOpts; use self::revoke::RevokeCommand; +mod create; +mod list; +mod revoke; +mod show; + #[derive(Clone, Debug, Args)] #[command(arg_required_else_help = true, subcommand_required = true)] pub struct LeaseCommand { @@ -61,31 +57,37 @@ async fn authenticate( cloud_opts: &CloudOpts, trust_opts: &TrustContextOpts, ) -> miette::Result { - let trust_context_config = trust_opts.to_config(&opts.state)?.build(); + let trust_context = opts + .state + .retrieve_trust_context( + &trust_opts.trust_context, + &trust_opts.project_name, + &None, + &None, + ) + .await?; + let node = InMemoryNode::start_with_trust_context( ctx, &opts.state, - trust_opts.project_path.as_ref(), - trust_context_config, + trust_opts.project_name(), + trust_context, ) .await?; - let identity = get_identity_name(&opts.state, &cloud_opts.identity); - let project_info = retrieve_project_info(opts, trust_opts).await?; - let project_authority = project_info - .authority - .as_ref() - .ok_or(miette!("Project Authority is required"))?; - let project_identifier = project_info - .identity_id - .ok_or(miette!("Project identifier is required"))?; - let project_addr = project_info - .node_route - .ok_or(miette!("Project route is required"))?; + let identity = opts + .state + .get_identity_name_or_default(&cloud_opts.identity) + .await?; + let project = node + .get_project_by_name_or_default(ctx, &trust_opts.project_name()) + .await?; + + let authority_identity = project.authority_identity().await.into_diagnostic()?; let authority_node = node .create_authority_client( - project_authority.identity_id(), - project_authority.address(), + authority_identity.identifier(), + &project.authority_access_route().into_diagnostic()?, Some(identity.clone()), ) .await?; @@ -93,32 +95,10 @@ async fn authenticate( authority_node .authenticate(ctx, Some(identity.clone())) .await?; - node.create_project_client(&project_identifier, &project_addr, Some(identity.clone())) - .await -} - -async fn retrieve_project_info( - opts: &CommandGlobalOpts, - trust_context_opts: &TrustContextOpts, -) -> miette::Result { - let project_path = match &trust_context_opts.project_path { - Some(p) => p.clone(), - None => { - let default_project = opts - .state - .projects - .default() - .context("A default project or project parameter is required")?; - - default_project.path().clone() - } - }; - // Read (okta and authority) project parameters from project.json - let s = tokio::fs::read_to_string(project_path) - .await - .into_diagnostic()?; - let proj_info: ProjectConfigCompact = serde_json::from_str(&s).into_diagnostic()?; - ProjectLookup::from_project(&(&proj_info).into()) - .await - .into_diagnostic() + node.create_project_client( + &project.identifier().into_diagnostic()?, + &project.access_route().into_diagnostic()?, + Some(identity.clone()), + ) + .await } diff --git a/implementations/rust/ockam/ockam_command/src/lease/revoke.rs b/implementations/rust/ockam/ockam_command/src/lease/revoke.rs index 25af5141d3e..70c3e1ec513 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/revoke.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/revoke.rs @@ -2,7 +2,6 @@ use clap::Args; use ockam::Context; use ockam_api::InfluxDbTokenLease; -use crate::identity::initialize_identity_if_default; use crate::lease::authenticate; use crate::util::api::{CloudOpts, TrustContextOpts}; use crate::util::node_rpc; @@ -21,7 +20,6 @@ pub struct RevokeCommand { impl RevokeCommand { pub fn run(self, opts: CommandGlobalOpts, cloud_opts: CloudOpts, trust_opts: TrustContextOpts) { - initialize_identity_if_default(&opts, &cloud_opts.identity); node_rpc(run_impl, (opts, cloud_opts, self, trust_opts)); } } diff --git a/implementations/rust/ockam/ockam_command/src/lease/show.rs b/implementations/rust/ockam/ockam_command/src/lease/show.rs index 508230dfe63..2c7f9b3b1b3 100644 --- a/implementations/rust/ockam/ockam_command/src/lease/show.rs +++ b/implementations/rust/ockam/ockam_command/src/lease/show.rs @@ -3,7 +3,6 @@ use clap::Args; use ockam::Context; use ockam_api::InfluxDbTokenLease; -use crate::identity::initialize_identity_if_default; use crate::lease::authenticate; use crate::output::Output; use crate::util::api::{CloudOpts, TrustContextOpts}; @@ -23,7 +22,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts, cloud_opts: CloudOpts, trust_opts: TrustContextOpts) { - initialize_identity_if_default(&opts, &cloud_opts.identity); node_rpc(run_impl, (opts, cloud_opts, self, trust_opts)); } } diff --git a/implementations/rust/ockam/ockam_command/src/lib.rs b/implementations/rust/ockam/ockam_command/src/lib.rs index 8d0596e5c1e..62664ecb1b9 100644 --- a/implementations/rust/ockam/ockam_command/src/lib.rs +++ b/implementations/rust/ockam/ockam_command/src/lib.rs @@ -17,6 +17,65 @@ //! cd implementations/rust/ockam/ockam_command && cargo install --path . //! ``` +use std::{path::PathBuf, sync::Mutex}; + +use clap::{ArgAction, Args, Parser, Subcommand}; +use colorful::Colorful; +use console::Term; +use miette::GraphicalReportHandler; +use once_cell::sync::Lazy; + +use authenticated::AuthenticatedCommand; +use completion::CompletionCommand; +use configuration::ConfigurationCommand; +use credential::CredentialCommand; +use enroll::EnrollCommand; +use environment::EnvironmentCommand; +use error::{Error, Result}; +use identity::IdentityCommand; +use kafka::consumer::KafkaConsumerCommand; +use kafka::producer::KafkaProducerCommand; +use lease::LeaseCommand; +use manpages::ManpagesCommand; +use markdown::MarkdownCommand; +use message::MessageCommand; +use node::NodeCommand; +use ockam_api::cli_state::CliState; +use ockam_core::env::get_env_with_default; +use policy::PolicyCommand; +use project::ProjectCommand; +use relay::RelayCommand; +use reset::ResetCommand; +use secure_channel::{listener::SecureChannelListenerCommand, SecureChannelCommand}; +use service::ServiceCommand; +#[cfg(feature = "orchestrator")] +use share::ShareCommand; +use space::SpaceCommand; +use status::StatusCommand; +use tcp::{ + connection::TcpConnectionCommand, inlet::TcpInletCommand, listener::TcpListenerCommand, + outlet::TcpOutletCommand, +}; +use trust_context::TrustContextCommand; +use upgrade::check_if_an_upgrade_is_available; +use util::{exitcode, exitcode::ExitCode}; +use vault::VaultCommand; +use version::Version; +use worker::WorkerCommand; + +use crate::admin::AdminCommand; +use crate::authority::AuthorityCommand; +use crate::flow_control::FlowControlCommand; +use crate::kafka::direct::KafkaDirectCommand; +use crate::kafka::outlet::KafkaOutletCommand; +use crate::logs::setup_logging; +use crate::node::NodeSubcommand; +use crate::output::{Output, OutputFormat}; +use crate::run::RunCommand; +use crate::sidecar::SidecarCommand; +use crate::subscription::SubscriptionCommand; +pub use crate::terminal::{OckamColor, Terminal, TerminalStream}; + mod admin; mod authenticated; mod authority; @@ -62,63 +121,6 @@ mod vault; mod version; mod worker; -use crate::admin::AdminCommand; -use crate::authority::AuthorityCommand; -use crate::flow_control::FlowControlCommand; -use crate::logs::setup_logging; -use crate::node::NodeSubcommand; -use crate::run::RunCommand; -use crate::subscription::SubscriptionCommand; -pub use crate::terminal::{OckamColor, Terminal, TerminalStream}; -use authenticated::AuthenticatedCommand; -use clap::{ArgAction, Args, Parser, Subcommand}; - -use crate::kafka::direct::KafkaDirectCommand; -use crate::kafka::outlet::KafkaOutletCommand; -use crate::output::{Output, OutputFormat}; -use crate::sidecar::SidecarCommand; -use colorful::Colorful; -use completion::CompletionCommand; -use configuration::ConfigurationCommand; -use console::Term; -use credential::CredentialCommand; -use enroll::EnrollCommand; -use environment::EnvironmentCommand; -use error::{Error, Result}; -use identity::IdentityCommand; -use kafka::consumer::KafkaConsumerCommand; -use kafka::producer::KafkaProducerCommand; -use lease::LeaseCommand; -use manpages::ManpagesCommand; -use markdown::MarkdownCommand; -use message::MessageCommand; -use miette::GraphicalReportHandler; -use node::NodeCommand; -use ockam_api::cli_state::CliState; -use ockam_core::env::get_env_with_default; -use once_cell::sync::Lazy; -use policy::PolicyCommand; -use project::ProjectCommand; -use relay::RelayCommand; -use reset::ResetCommand; -use secure_channel::{listener::SecureChannelListenerCommand, SecureChannelCommand}; -use service::ServiceCommand; -#[cfg(feature = "orchestrator")] -use share::ShareCommand; -use space::SpaceCommand; -use status::StatusCommand; -use std::{path::PathBuf, sync::Mutex}; -use tcp::{ - connection::TcpConnectionCommand, inlet::TcpInletCommand, listener::TcpListenerCommand, - outlet::TcpOutletCommand, -}; -use trust_context::TrustContextCommand; -use upgrade::check_if_an_upgrade_is_available; -use util::{exitcode, exitcode::ExitCode}; -use vault::VaultCommand; -use version::Version; -use worker::WorkerCommand; - const ABOUT: &str = include_str!("./static/about.txt"); const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/after_long_help.txt"); @@ -240,7 +242,7 @@ pub struct CommandGlobalOpts { impl CommandGlobalOpts { pub fn new(global_args: GlobalArgs) -> Self { - let state = match CliState::initialize() { + let state = match CliState::with_default_dir() { Ok(state) => state, Err(err) => { eprintln!("Failed to initialize state: {}", err); @@ -250,7 +252,7 @@ impl CommandGlobalOpts { let state = CliState::backup_and_reset().expect( "Failed to initialize CliState. Try to manually remove the '~/.ockam' directory", ); - let dir = &state.dir; + let dir = state.dir(); let backup_dir = CliState::backup_default_dir().unwrap(); eprintln!( "The {dir:?} directory has been reset and has been backed up to {backup_dir:?}" @@ -494,13 +496,9 @@ impl OckamCommand { } // In the case where a node is explicitly created in foreground mode, we need // to initialize the node directories before we can get the log path. - let path = opts - .state - .nodes - .stdout_logs(&c.node_name) - .unwrap_or_else(|_| { - panic!("Failed to initialize logs file for node {}", c.node_name) - }); + let path = opts.state.stdout_logs(&c.node_name).unwrap_or_else(|_| { + panic!("Failed to initialize logs file for node {}", c.node_name) + }); return Some(path); } } @@ -518,6 +516,7 @@ pub(crate) fn display_parse_logs(opts: &CommandGlobalOpts) { logs.clear(); } } + pub(crate) fn replace_hyphen_with_stdin(s: String) -> String { let input_stream = std::io::stdin(); if s.contains("/-") { diff --git a/implementations/rust/ockam/ockam_command/src/message/send.rs b/implementations/rust/ockam/ockam_command/src/message/send.rs index 49a32529617..670d9cfa022 100644 --- a/implementations/rust/ockam/ockam_command/src/message/send.rs +++ b/implementations/rust/ockam/ockam_command/src/message/send.rs @@ -2,6 +2,7 @@ use core::time::Duration; use clap::Args; use miette::{Context as _, IntoDiagnostic}; +use tracing::info; use ockam::Context; use ockam_api::address::extract_address_value; @@ -11,8 +12,6 @@ use ockam_api::nodes::InMemoryNode; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::identity::{get_identity_name, initialize_identity_if_default}; - use crate::project::util::{ clean_projects_multiaddr, get_projects_secure_channels_from_config_lookup, }; @@ -33,7 +32,7 @@ after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct SendCommand { /// The node to send messages from - #[arg(short, long, value_name = "NODE")] + #[arg(short, long, value_name = "NODE", value_parser = extract_address_value)] from: Option, /// The route to send the message to @@ -59,7 +58,6 @@ pub struct SendCommand { impl SendCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.cloud_opts.identity); node_rpc(rpc, (opts, self)) } } @@ -67,8 +65,9 @@ impl SendCommand { async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, SendCommand)) -> miette::Result<()> { async fn go(ctx: &Context, opts: CommandGlobalOpts, cmd: SendCommand) -> miette::Result<()> { // Process `--to` Multiaddr - let (to, meta) = - clean_nodes_multiaddr(&cmd.to, &opts.state).context("Argument '--to' is invalid")?; + let (to, meta) = clean_nodes_multiaddr(&cmd.to, &opts.state) + .await + .context("Argument '--to' is invalid")?; let msg_bytes = if cmd.hex { hex::decode(cmd.message) @@ -78,28 +77,45 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, SendCommand)) -> mie cmd.message.as_bytes().to_vec() }; - // Setup environment depending on whether we are sending the message from an background node + // Setup environment depending on whether we are sending the message from a background node // or an in-memory node let response: Vec = if let Some(node) = &cmd.from { - let node_name = extract_address_value(node)?; - BackgroundNode::create(ctx, &opts.state, &node_name) + BackgroundNode::create_to_node(ctx, &opts.state, node.as_str()) .await? .set_timeout(cmd.timeout) .ask(ctx, req(&to, msg_bytes)) .await? } else { - let identity_name = get_identity_name(&opts.state, &cmd.cloud_opts.identity); - let trust_context_config = cmd.trust_context_opts.to_config(&opts.state)?.build(); + let identity_name = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; + + info!("retrieving the trust context"); + + let named_trust_context = opts + .state + .retrieve_trust_context( + &cmd.trust_context_opts.trust_context, + &cmd.trust_context_opts.project_name, + &None, + &None, + ) + .await?; + info!("retrieved the trust context: {named_trust_context:?}"); + + info!("starting an in memory node to send a message"); let node_manager = InMemoryNode::start_node( ctx, &opts.state, - None, Some(identity_name.clone()), - cmd.trust_context_opts.project_path.as_ref(), - trust_context_config, + None, + cmd.trust_context_opts.project_name, + named_trust_context, ) .await?; + info!("started an in memory node to send a message"); // Replace `/project/` occurrences with their respective secure channel addresses let projects_sc = get_projects_secure_channels_from_config_lookup( @@ -112,6 +128,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, SendCommand)) -> mie ) .await?; let to = clean_projects_multiaddr(to, projects_sc)?; + info!("sending to {to}"); node_manager .send_message(ctx, &to, msg_bytes, Some(cmd.timeout)) .await diff --git a/implementations/rust/ockam/ockam_command/src/node/create.rs b/implementations/rust/ockam/ockam_command/src/node/create.rs index 864d07a0a88..d125763c6ac 100644 --- a/implementations/rust/ockam/ockam_command/src/node/create.rs +++ b/implementations/rust/ockam/ockam_command/src/node/create.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use std::{path::PathBuf, process, str::FromStr}; +use std::{path::PathBuf, str::FromStr}; use clap::Args; use colorful::Colorful; @@ -9,18 +9,17 @@ use minicbor::{Decoder, Encode}; use tokio::sync::Mutex; use tokio::time::{sleep, Duration}; use tokio::try_join; +use tracing::{debug, info}; +use ockam::identity::Identity; use ockam::{Address, AsyncTryClone, TcpListenerOptions}; use ockam::{Context, TcpTransport}; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_api::cli_state::{add_project_info_to_node_state, init_node_state, random_name}; -use ockam_api::nodes::models::transport::CreateTransportJson; +use ockam_api::cli_state::random_name; use ockam_api::nodes::service::NodeManagerTrustOptions; use ockam_api::nodes::BackgroundNode; use ockam_api::nodes::InMemoryNode; use ockam_api::{ bootstrapped_identities_store::PreTrustedIdentities, - nodes::models::transport::{TransportMode, TransportType}, nodes::{ service::{NodeManagerGeneralOptions, NodeManagerTransportOptions}, NodeManagerWorker, NODEMANAGER_ADDR, @@ -29,18 +28,18 @@ use ockam_api::{ use ockam_core::api::{Request, ResponseHeader, Status}; use ockam_core::{route, LOCAL}; +use crate::node::show::is_node_up; use crate::node::util::{spawn_node, NodeManagerDefaults}; use crate::secure_channel::listener::create as secure_channel_listener; use crate::service::config::Config; use crate::terminal::OckamColor; use crate::util::api::TrustContextOpts; -use crate::util::{api, parse_node_name}; -use crate::util::{embedded_node_that_is_not_stopped, exitcode}; +use crate::util::embedded_node_that_is_not_stopped; +use crate::util::parsers::identity_parser; +use crate::util::{api, exitcode}; use crate::util::{local_cmd, node_rpc}; -use crate::{docs, shutdown, CommandGlobalOpts, Result}; -use crate::{fmt_log, fmt_ok}; - -use super::show::is_node_up; +use crate::{docs, fmt_log, fmt_ok}; +use crate::{shutdown, CommandGlobalOpts, Result}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -101,8 +100,8 @@ pub struct CreateCommand { #[arg(long = "identity", value_name = "IDENTITY_NAME")] identity: Option, - #[arg(long)] - pub authority_identity: Option, + #[arg(long, value_name = "IDENTITY", value_parser = identity_parser)] + pub authority_identity: Option, #[arg(long = "credential", value_name = "CREDENTIAL_NAME")] pub credential: Option, @@ -123,10 +122,10 @@ impl Default for CreateCommand { launch_config: None, vault: None, identity: None, + authority_identity: None, trusted_identities: None, trusted_identities_file: None, reload_from_trusted_identities_file: None, - authority_identity: None, credential: None, trust_context_opts: node_manager_defaults.trust_context_opts, } @@ -135,17 +134,6 @@ impl Default for CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - if !self.child_process { - if let Ok(state) = opts.state.nodes.get(&self.node_name) { - if state.is_running() { - eprintln!( - "{:?}", - miette!("Node {} is already running", self.node_name) - ); - std::process::exit(exitcode::SOFTWARE); - } - } - } if self.foreground { local_cmd(foreground_mode(opts, self)); } else { @@ -187,12 +175,14 @@ pub(crate) async fn background_mode( ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let node_name = &parse_node_name(&cmd.node_name)?; + guard_node_is_not_already_running(&opts, &cmd).await; + + let node_name = cmd.node_name.clone(); + debug!("create node in background mode"); + opts.terminal.write_line(&fmt_log!( "Creating Node {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node_name.clone().color(OckamColor::PrimaryResource.color()) ))?; if cmd.child_process { @@ -205,8 +195,8 @@ pub(crate) async fn background_mode( let send_req = async { spawn_background_node(&opts, cmd.clone()).await?; - let mut node = BackgroundNode::create(&ctx, &opts.state, node_name).await?; - let is_node_up = is_node_up(&ctx, node_name, &mut node, opts.state.clone(), true).await?; + let mut node = BackgroundNode::create_to_node(&ctx, &opts.state, &node_name).await?; + let is_node_up = is_node_up(&ctx, &mut node, true).await?; *is_finished.lock().await = true; Ok(is_node_up) }; @@ -229,9 +219,7 @@ pub(crate) async fn background_mode( .plain( fmt_ok!( "Node {} created successfully\n\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node_name.color(OckamColor::PrimaryResource.color()) ) + &fmt_log!("To see more details on this node, run:\n") + &fmt_log!( "{}", @@ -253,33 +241,42 @@ async fn run_foreground_node( ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let node_name = parse_node_name(&cmd.node_name)?; + guard_node_is_not_already_running(&opts, &cmd).await; + + let node_name = cmd.node_name.clone(); + debug!("create node {node_name} in foreground mode"); - // This node was initially created as a foreground node - // and there is no existing state for it yet. - if !cmd.child_process && !opts.state.nodes.exists(&node_name) { - init_node_state( - &opts.state, + if opts + .state + .get_node(&node_name) + .await + .ok() + .map(|n| n.is_running()) + .unwrap_or(false) + { + eprintln!("{:?}", miette!("Node {} is already running", &node_name)); + std::process::exit(exitcode::SOFTWARE); + }; + + let node_info = opts + .state + .create_node_with_optional_values( &node_name, - cmd.vault.as_deref(), - cmd.identity.as_deref(), + &cmd.identity, + &cmd.trust_context_opts.project_name, + ) + .await?; + debug!("created node {node_info:?}"); + + let named_trust_context = opts + .state + .retrieve_trust_context( + &cmd.trust_context_opts.trust_context, + &cmd.trust_context_opts.project_name, + &cmd.authority_identity, + &cmd.credential, ) .await?; - } - - add_project_info_to_node_state( - &node_name, - &opts.state, - cmd.trust_context_opts.project_path.as_ref(), - ) - .await?; - - let trust_context_config = cmd - .trust_context_opts - .to_config(&opts.state)? - .with_authority_identity(cmd.authority_identity.as_ref()) - .with_credential_name(cmd.credential.as_ref()) - .build(); let tcp = TcpTransport::create(&ctx).await.into_diagnostic()?; let options = TcpListenerOptions::new(); @@ -288,22 +285,13 @@ async fn run_foreground_node( .await .into_diagnostic()?; - let node_state = opts.state.nodes.get(&node_name)?; - node_state.set_pid(process::id() as i32)?; - node_state.set_setup( - &node_state - .config() - .setup_mut() - .set_verbose(opts.global_args.verbose) - .set_api_transport( - CreateTransportJson::new( - TransportType::Tcp, - TransportMode::Listen, - &listener.socket_address().to_string(), - ) - .into_diagnostic()?, - ), - )?; + opts.state + .set_tcp_listener_address(&node_name, listener.socket_address().to_string()) + .await?; + debug!( + "set the node {node_name} listener address to {:?}", + listener.socket_address() + ); let pre_trusted_identities = load_pre_trusted_identities(&cmd)?; @@ -311,7 +299,7 @@ async fn run_foreground_node( &ctx, NodeManagerGeneralOptions::new( opts.state.clone(), - cmd.node_name.clone(), + node_name.clone(), pre_trusted_identities, cmd.launch_config.is_none(), true, @@ -320,7 +308,7 @@ async fn run_foreground_node( listener.flow_control_id().clone(), tcp.async_try_clone().await.into_diagnostic()?, ), - NodeManagerTrustOptions::new(trust_context_config), + NodeManagerTrustOptions::new(named_trust_context), ) .await .into_diagnostic()?; @@ -360,9 +348,7 @@ async fn run_foreground_node( .await?; // Try to stop node; it might have already been stopped or deleted (e.g. when running `node delete --all`) - if let Ok(state) = opts.state.nodes.get(&node_name) { - let _ = state.kill_process(false); - } + opts.state.stop_node(&node_name, true).await?; ctx.stop().await.into_diagnostic()?; opts.terminal .write_line(format!("{}Node stopped successfully", "✔︎".light_green()).as_str()) @@ -441,43 +427,49 @@ pub async fn spawn_background_node( opts: &CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - let node_name = parse_node_name(&cmd.node_name)?; - // Create node state, including the vault and identity if don't exist - init_node_state( - &opts.state, - &node_name, - cmd.vault.as_deref(), - cmd.identity.as_deref(), - ) - .await?; - - let trust_context_path = match cmd.trust_context_opts.trust_context.clone() { + let trust_context = match cmd.trust_context_opts.trust_context.clone() { Some(tc) => { - let config = opts.state.trust_contexts.read_config_from_path(&tc)?; - Some(config.path().unwrap().clone()) + let trust_context = opts.state.get_trust_context(&tc).await?; + Some(trust_context) } None => None, }; // Construct the arguments list and re-execute the ockam // CLI in foreground mode to start the newly created node + info!("spawing a new node {}", &cmd.node_name); spawn_node( opts, - &node_name, + &cmd.node_name, + &cmd.identity, + &cmd.vault, &cmd.tcp_listener_address, - cmd.trust_context_opts.project_path.as_ref(), cmd.trusted_identities.as_ref(), cmd.trusted_identities_file.as_ref(), cmd.reload_from_trusted_identities_file.as_ref(), cmd.launch_config .as_ref() .map(|config| serde_json::to_string(config).unwrap()), - cmd.authority_identity.as_ref(), cmd.credential.as_ref(), - trust_context_path.as_ref(), - cmd.trust_context_opts.project.as_ref(), + trust_context.as_ref(), + cmd.trust_context_opts.project_name.clone(), cmd.logging_to_file(), - )?; + ) + .await?; Ok(()) } + +async fn guard_node_is_not_already_running(opts: &CommandGlobalOpts, cmd: &CreateCommand) { + if !cmd.child_process { + if let Ok(node) = opts.state.get_node(&cmd.node_name).await { + if node.is_running() { + eprintln!( + "{:?}", + miette!("Node {} is already running", &cmd.node_name) + ); + std::process::exit(exitcode::SOFTWARE); + } + } + } +} diff --git a/implementations/rust/ockam/ockam_command/src/node/default.rs b/implementations/rust/ockam/ockam_command/src/node/default.rs index 433d127fff7..4f9a55ca0b3 100644 --- a/implementations/rust/ockam/ockam_command/src/node/default.rs +++ b/implementations/rust/ockam/ockam_command/src/node/default.rs @@ -1,11 +1,11 @@ -use crate::node::{get_default_node_name, get_node_name}; -use crate::util::local_cmd; -use crate::{docs, fmt_ok, CommandGlobalOpts}; - use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::StateDirTrait; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/default/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt"); @@ -13,8 +13,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt /// Change the default node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct DefaultCommand { /// Name of the node to set as default @@ -23,25 +23,34 @@ pub struct DefaultCommand { impl DefaultCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DefaultCommand) -> miette::Result<()> { +async fn run_impl( + _cxt: Context, + (opts, cmd): (CommandGlobalOpts, DefaultCommand), +) -> miette::Result<()> { if let Some(node_name) = cmd.node_name { - let name = get_node_name(&opts.state, &Some(node_name.clone())); - if opts.state.nodes.is_default(&name)? { - return Err(miette!("The node '{name}' is already the default")); + if opts + .state + .get_node(&node_name) + .await + .ok() + .map(|n| n.is_default()) + .unwrap_or(false) + { + return Err(miette!("The node '{node_name}' is already the default")); } else { - opts.state.nodes.set_default(&name)?; + opts.state.set_default_node(&node_name).await?; opts.terminal .stdout() - .plain(fmt_ok!("The node '{name}' is now the default")) - .machine(&name) + .plain(fmt_ok!("The node '{node_name}' is now the default")) + .machine(&node_name) .write_line()?; } } else { - let default_node_name = get_default_node_name(&opts.state); + let default_node_name = opts.state.get_default_node().await?.name(); let _ = opts .terminal .stdout() diff --git a/implementations/rust/ockam/ockam_command/src/node/delete.rs b/implementations/rust/ockam/ockam_command/src/node/delete.rs index f2214e06837..f0925a3d155 100644 --- a/implementations/rust/ockam/ockam_command/src/node/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/node/delete.rs @@ -1,10 +1,8 @@ use clap::Args; use colorful::Colorful; use console::Term; -use ockam_api::cli_state::StateDirTrait; use ockam_node::Context; -use crate::node::get_node_name; use crate::terminal::tui::DeleteCommandTui; use crate::util::node_rpc; use crate::{docs, fmt_ok, fmt_warn, CommandGlobalOpts, Terminal, TerminalStream}; @@ -82,18 +80,30 @@ impl DeleteCommandTui for DeleteTui { } async fn get_arg_item_name_or_default(&self) -> miette::Result { - Ok(get_node_name(&self.opts.state, &self.cmd.node_name)) + Ok(self + .opts + .state + .get_node_or_default(&self.cmd.node_name) + .await? + .name()) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.nodes.list_items_names()?) + Ok(self + .opts + .state + .get_nodes() + .await? + .iter() + .map(|n| n.name()) + .collect()) } async fn delete_single(&self, item_name: &str) -> miette::Result<()> { self.opts .state - .nodes - .delete_sigkill(item_name, self.cmd.force)?; + .delete_node(item_name, self.cmd.force) + .await?; self.terminal() .stdout() .plain(fmt_ok!( @@ -107,23 +117,26 @@ impl DeleteCommandTui for DeleteTui { } async fn delete_multiple(&self, items_names: Vec) -> miette::Result<()> { - let plain = items_names - .into_iter() - .map(|name| { - if self - .opts - .state - .nodes - .delete_sigkill(&name, self.cmd.force) - .is_ok() - { - fmt_ok!("Node {} deleted\n", name.light_magenta()) - } else { - fmt_warn!("Failed to delete node {}\n", name.light_magenta()) - } - }) - .collect::(); - self.terminal().stdout().plain(plain).write_line()?; + let mut plain: Vec = vec![]; + for name in items_names { + let result = if self + .opts + .state + .delete_node(&name, self.cmd.force) + .await + .is_ok() + { + fmt_ok!("Node {} deleted\n", name.light_magenta()) + } else { + fmt_warn!("Failed to delete node {}\n", name.light_magenta()) + }; + plain.push(result); + } + + self.terminal() + .stdout() + .plain(plain.join("\n")) + .write_line()?; Ok(()) } } diff --git a/implementations/rust/ockam/ockam_command/src/node/list.rs b/implementations/rust/ockam/ockam_command/src/node/list.rs index 5def071cd20..b0da2200bf7 100644 --- a/implementations/rust/ockam/ockam_command/src/node/list.rs +++ b/implementations/rust/ockam/ockam_command/src/node/list.rs @@ -1,21 +1,17 @@ use clap::Args; use colorful::Colorful; use indoc::formatdoc; -use miette::Context as _; use miette::IntoDiagnostic; use serde::Serialize; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; -use ockam_api::nodes::models::base::NodeStatus; -use ockam_api::nodes::BackgroundNode; +use ockam_api::cli_state::nodes::NodeInfo; -use crate::node::get_default_node_name; use crate::output::Output; use crate::terminal::OckamColor; -use crate::util::{api, node_rpc}; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts, Result}; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); @@ -25,9 +21,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List nodes #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand {} @@ -38,7 +34,7 @@ impl ListCommand { } async fn run_impl( - ctx: Context, + _ctx: Context, (opts, _cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { // Before printing node states we verify them. @@ -48,47 +44,28 @@ async fn run_impl( // This should only happen if the node has failed in the past, // and has been restarted by something that is not this CLI. let node_names: Vec<_> = { - let nodes_states = opts.state.nodes.list()?; - nodes_states.iter().map(|s| s.name().to_string()).collect() + let nodes = opts.state.get_nodes().await?; + nodes.iter().map(|n| n.name()).collect() }; - let nodes = get_nodes_info(&ctx, &opts, node_names).await?; + let nodes = get_nodes_info(&opts, node_names).await?; print_nodes_info(&opts, nodes)?; - Ok(()) } pub async fn get_nodes_info( - ctx: &Context, opts: &CommandGlobalOpts, node_names: Vec, ) -> Result> { let mut nodes: Vec = Vec::new(); - let default_node_name = get_default_node_name(&opts.state); - for node_name in node_names { - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + for node_name in node_names { let is_finished: Mutex = Mutex::new(false); let get_node_status = async { - let result: miette::Result = node.ask(ctx, api::query_status()).await; - let node_status = match result { - Ok(node_status) => { - if let Ok(node_state) = opts.state.nodes.get(&node_name) { - // Update the persisted configuration data with the pids - // responded by nodes. - if node_state.pid()? != Some(node_status.pid) { - node_state - .set_pid(node_status.pid) - .context("Failed to update pid for node {node_name}")?; - } - } - node_status - } - Err(_) => NodeStatus::new(node_name.to_string(), "Not running".to_string(), 0, 0), - }; + let node = opts.state.get_node(&node_name).await?; *is_finished.lock().await = true; - Ok(node_status) + Ok(node) }; let output_messages = vec![format!( @@ -101,14 +78,9 @@ pub async fn get_nodes_info( .terminal .progress_output(&output_messages, &is_finished); - let (node_status, _) = try_join!(get_node_status, progress_output)?; + let (node, _) = try_join!(get_node_status, progress_output)?; - nodes.push(NodeListOutput::new( - node_status.node_name.to_string(), - node_status.status.to_string(), - node_status.pid, - node_status.node_name == default_node_name, - )); + nodes.push(NodeListOutput::from_node_info(&node)); } Ok(nodes) @@ -138,12 +110,12 @@ pub fn print_nodes_info( pub struct NodeListOutput { pub node_name: String, pub status: String, - pub pid: i32, + pub pid: Option, pub is_default: bool, } impl NodeListOutput { - pub fn new(node_name: String, status: String, pid: i32, is_default: bool) -> Self { + pub fn new(node_name: String, status: String, pid: Option, is_default: bool) -> Self { Self { node_name, status, @@ -151,18 +123,30 @@ impl NodeListOutput { is_default, } } + + pub fn from_node_info(node_info: &NodeInfo) -> Self { + let status = if node_info.is_running() { + "Running" + } else { + "Not running" + }; + Self::new( + node_info.name(), + status.to_string(), + node_info.pid(), + node_info.is_default(), + ) + } } impl Output for NodeListOutput { fn output(&self) -> Result { - let (status, pid) = match self.status.as_str() { - "Running" => ( + let (status, pid) = match self.pid { + Some(pid) => ( "UP".color(OckamColor::Success.color()), format!( "Process id {}", - self.pid - .to_string() - .color(OckamColor::PrimaryResource.color()) + pid.to_string().color(OckamColor::PrimaryResource.color()) ), ), _ => ( diff --git a/implementations/rust/ockam/ockam_command/src/node/logs.rs b/implementations/rust/ockam/ockam_command/src/node/logs.rs index d0b944a79d4..37466902353 100644 --- a/implementations/rust/ockam/ockam_command/src/node/logs.rs +++ b/implementations/rust/ockam/ockam_command/src/node/logs.rs @@ -1,10 +1,11 @@ -use crate::fmt_ok; -use crate::node::get_node_name; -use crate::util::local_cmd; -use crate::{docs, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::StateDirTrait; + +use ockam_node::Context; + +use crate::fmt_ok; +use crate::util::node_rpc; +use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/logs/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -13,9 +14,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/logs/after_long_help.txt"); /// Get the stdout/stderr log file of a node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct LogCommand { /// Name of the node to retrieve the logs from. @@ -24,14 +25,16 @@ pub struct LogCommand { impl LogCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: LogCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_name); - let node_state = opts.state.nodes.get(node_name)?; - let log_path = node_state.stdout_log().display().to_string(); +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, LogCommand), +) -> miette::Result<()> { + let node_name = opts.state.get_node_or_default(&cmd.node_name).await?.name(); + let log_path = opts.state.stdout_logs(&node_name)?.display().to_string(); opts.terminal .stdout() .plain(fmt_ok!("The path for the log file is: {log_path}")) diff --git a/implementations/rust/ockam/ockam_command/src/node/mod.rs b/implementations/rust/ockam/ockam_command/src/node/mod.rs index ebd836ddad6..ce3d1ea4e8e 100644 --- a/implementations/rust/ockam/ockam_command/src/node/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/node/mod.rs @@ -1,17 +1,17 @@ use clap::{Args, Subcommand}; +use ockam_api::address::extract_address_value; -use colorful::Colorful; pub use create::CreateCommand; +pub use create::*; use default::DefaultCommand; use delete::DeleteCommand; use list::ListCommand; use logs::LogCommand; -use ockam_api::cli_state::{CliState, StateDirTrait}; use show::ShowCommand; use start::StartCommand; use stop::StopCommand; -use crate::{docs, fmt_log, terminal::OckamColor, CommandGlobalOpts, PARSER_LOGS}; +use crate::{docs, CommandGlobalOpts}; mod create; mod default; @@ -23,7 +23,6 @@ mod show; mod start; mod stop; pub mod util; -pub use create::*; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/after_long_help.txt"); @@ -31,10 +30,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/after_long_help.txt"); /// Manage Nodes #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - subcommand_required = true, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +arg_required_else_help = true, +subcommand_required = true, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct NodeCommand { #[command(subcommand)] @@ -78,85 +77,6 @@ impl NodeCommand { #[derive(Clone, Debug, Args)] pub struct NodeOpts { /// Perform the command on the given NODE_NAME - #[arg(global = true, id = "at", value_name = "NODE_NAME", long)] + #[arg(global = true, id = "at", value_name = "NODE_NAME", long, value_parser = extract_address_value)] pub at_node: Option, } - -/// If the required node name is the default node but that node has not been initialized yet -/// then initialize it -pub fn initialize_node_if_default(opts: &CommandGlobalOpts, node_name: &Option) { - let node_name = get_node_name(&opts.state, node_name); - if node_name == "default" && opts.state.nodes.default().is_err() { - spawn_default_node(opts) - } -} - -/// Return the node_name if Some otherwise return the default node name -pub fn get_node_name<'a>(cli_state: &CliState, node_name: impl Into<&'a Option>) -> String { - node_name - .into() - .clone() - .unwrap_or_else(|| get_default_node_name(cli_state)) -} - -/// Return the default node name -pub fn get_default_node_name(cli_state: &CliState) -> String { - cli_state - .nodes - .default() - .map(|n| n.name().to_string()) - .unwrap_or_else(|_| "default".to_string()) -} - -/// Start the default node -fn spawn_default_node(opts: &CommandGlobalOpts) { - let mut create_command = CreateCommand::default(); - - let default = "default"; - create_command.node_name = default.into(); - create_command.run(opts.clone().set_quiet()); - - if let Ok(mut logs) = PARSER_LOGS.lock() { - logs.push(fmt_log!( - "There is no node, on this machine, marked as your default." - )); - logs.push(fmt_log!("Creating a new Ockam node for you...")); - logs.push(fmt_log!( - "Created a new node named {}", - default.color(OckamColor::PrimaryResource.color()) - )); - logs.push(fmt_log!( - "Marked this node as your default, on this machine.\n" - )); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::GlobalArgs; - use ockam_api::cli_state::StateItemTrait; - - #[test] - fn test_initialize() { - let opts = CommandGlobalOpts::new(GlobalArgs::default()).set_quiet(); - - // on start-up there is no default node - let _ = opts.state.nodes.default().and_then(|n| n.delete()); - assert!(opts.state.nodes.default().is_err()); - - // if no name is given then the default node is initialized - initialize_node_if_default(&opts, &None); - assert!(opts.state.nodes.default().is_ok()); - - // if "default" is given as a name the default node is initialized - opts.state.nodes.default().unwrap().delete().unwrap(); - initialize_node_if_default(&opts, &Some("default".into())); - assert!(opts.state.nodes.default().is_ok()); - - // if the name of another identity is given then the default node is not initialized - opts.state.nodes.default().unwrap().delete().unwrap(); - initialize_node_if_default(&opts, &Some("other".into())); - assert!(opts.state.nodes.default().is_err()); - } -} diff --git a/implementations/rust/ockam/ockam_command/src/node/show.rs b/implementations/rust/ockam/ockam_command/src/node/show.rs index 2b4209c3d85..d9d1705cc9c 100644 --- a/implementations/rust/ockam/ockam_command/src/node/show.rs +++ b/implementations/rust/ockam/ockam_command/src/node/show.rs @@ -1,18 +1,17 @@ use clap::Args; use console::Term; use miette::IntoDiagnostic; +use tokio_retry::strategy::FixedInterval; +use tracing::{info, trace, warn}; + +use ockam_api::nodes::models::base::NodeStatus; +use ockam_api::nodes::models::portal::{InletList, OutletList}; use ockam_api::nodes::models::secure_channel::SecureChannelListenersList; use ockam_api::nodes::models::services::ServiceList; use ockam_api::nodes::models::transport::TransportList; use ockam_api::nodes::BackgroundNode; use ockam_node::Context; -use tokio_retry::strategy::FixedInterval; -use tracing::{info, trace, warn}; - -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; -use ockam_api::nodes::models::portal::{InletList, OutletList}; -use crate::node::get_node_name; use crate::node::list; use crate::terminal::tui::ShowCommandTui; use crate::util::{api, node_rpc}; @@ -34,9 +33,9 @@ const IS_NODE_UP_MAX_ATTEMPTS: usize = 60; // 3 seconds /// Show the details of a node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ShowCommand { /// Name of the node to retrieve the details from @@ -90,21 +89,35 @@ impl ShowCommandTui for ShowTui { } async fn get_arg_item_name_or_default(&self) -> miette::Result { - Ok(get_node_name(&self.opts.state, &self.node_name)) + Ok(self + .opts + .state + .get_node_or_default(&self.node_name) + .await? + .name()) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.nodes.list_items_names()?) + Ok(self + .opts + .state + .get_nodes() + .await? + .iter() + .map(|n| n.name()) + .collect()) } async fn show_single(&self, item_name: &str) -> miette::Result<()> { - let mut node = BackgroundNode::create(&self.ctx, &self.opts.state, item_name).await?; - print_query_status(&self.opts, &self.ctx, item_name, &mut node, false).await?; + let mut node = + BackgroundNode::create(&self.ctx, &self.opts.state, &Some(item_name.to_string())) + .await?; + print_query_status(&self.opts, &self.ctx, &mut node, false).await?; Ok(()) } async fn show_multiple(&self, items_names: Vec) -> miette::Result<()> { - let nodes = list::get_nodes_info(&self.ctx, &self.opts, items_names).await?; + let nodes = list::get_nodes_info(&self.opts, items_names).await?; list::print_nodes_info(&self.opts, nodes)?; Ok(()) } @@ -113,90 +126,81 @@ impl ShowCommandTui for ShowTui { pub async fn print_query_status( opts: &CommandGlobalOpts, ctx: &Context, - node_name: &str, node: &mut BackgroundNode, wait_until_ready: bool, ) -> miette::Result<()> { let cli_state = opts.state.clone(); - let is_default = opts.state.nodes.is_default(node_name)?; - - let node_info = - if !is_node_up(ctx, node_name, node, cli_state.clone(), wait_until_ready).await? { - let node_state = cli_state.nodes.get(node_name)?; - let node_port = node_state - .config() - .setup() - .api_transport() - .ok() - .map(|listener| listener.addr.port()); - - // it is expected to not be able to open an arbitrary TCP connection on an authority node - // so in that case we display an UP status - let is_authority_node = node_state.config().setup().authority_node.unwrap_or(false); - - ShowNodeResponse::new(is_default, node_name, is_authority_node, node_port) - } else { - let node_state = cli_state.nodes.get(node_name)?; - let node_port = node_state - .config() - .setup() - .api_transport() - .ok() - .map(|listener| listener.addr.port()); - - let mut node_info = ShowNodeResponse::new(is_default, node_name, true, node_port); - - // Get short id for the node - node_info.identity = Some(match node_state.config().identity_config() { - Ok(resp) => resp.identifier().to_string(), - Err(_) => String::from("None"), - }); - - // Get list of services for the node - let services: ServiceList = node.ask(ctx, api::list_services()).await?; - node_info.services = services - .list - .into_iter() - .map(ShowServiceStatus::from) - .collect(); - - // Get list of TCP listeners for node - let transports: TransportList = node.ask(ctx, api::list_tcp_listeners()).await?; - node_info.transports = transports - .list - .into_iter() - .map(ShowTransportStatus::from) - .collect(); - - // Get list of Secure Channel Listeners - let listeners: SecureChannelListenersList = - node.ask(ctx, api::list_secure_channel_listener()).await?; - node_info.secure_channel_listeners = listeners - .list - .into_iter() - .map(ShowSecureChannelListener::from) - .collect(); - - // Get list of inlets - let inlets: InletList = node.ask(ctx, api::list_inlets()).await?; - node_info.inlets = inlets.list.into_iter().map(ShowInletStatus::from).collect(); - - // Get list of outlets - let outlets: OutletList = node.ask(ctx, api::list_outlets()).await?; - node_info.outlets = outlets - .list - .into_iter() - .map(ShowOutletStatus::from) - .collect(); - - node_info - }; + let node_name = node.node_name(); + let node_info = cli_state.get_node(&node_name).await?; + + let show_node = if !is_node_up(ctx, node, wait_until_ready).await? { + // it is expected to not be able to open an arbitrary TCP connection on an authority node + // so in that case we display an UP status + let is_authority_node = cli_state + .get_node(&node_name) + .await + .ok() + .map(|n| n.is_authority_node()) + .unwrap_or(false); + + ShowNodeResponse::new( + node_info.is_default(), + &node_name, + is_authority_node, + node_info.tcp_listener_port(), + ) + } else { + let mut show_node = ShowNodeResponse::new( + node_info.is_default(), + &node_name, + true, + node_info.tcp_listener_port(), + ); + // Get list of services for the node + let services: ServiceList = node.ask(ctx, api::list_services()).await?; + show_node.services = services + .list + .into_iter() + .map(ShowServiceStatus::from) + .collect(); + + // Get list of TCP listeners for node + let transports: TransportList = node.ask(ctx, api::list_tcp_listeners()).await?; + show_node.transports = transports + .list + .into_iter() + .map(ShowTransportStatus::from) + .collect(); + + // Get list of Secure Channel Listeners + let listeners: SecureChannelListenersList = + node.ask(ctx, api::list_secure_channel_listener()).await?; + show_node.secure_channel_listeners = listeners + .list + .into_iter() + .map(ShowSecureChannelListener::from) + .collect(); + + // Get list of inlets + let inlets: InletList = node.ask(ctx, api::list_inlets()).await?; + show_node.inlets = inlets.list.into_iter().map(ShowInletStatus::from).collect(); + + // Get list of outlets + let outlets: OutletList = node.ask(ctx, api::list_outlets()).await?; + show_node.outlets = outlets + .list + .into_iter() + .map(ShowOutletStatus::from) + .collect(); + + show_node + }; opts.terminal .clone() .stdout() - .plain(&node_info) - .json(serde_json::to_string_pretty(&node_info).into_diagnostic()?) + .plain(&show_node) + .json(serde_json::to_string_pretty(&show_node).into_diagnostic()?) .write_line()?; Ok(()) @@ -211,9 +215,7 @@ pub async fn print_query_status( /// allow a node time to start up and become ready. pub async fn is_node_up( ctx: &Context, - node_name: &str, - node: &mut BackgroundNode, - cli_state: CliState, + node_client: &mut BackgroundNode, wait_until_ready: bool, ) -> Result { let attempts = match wait_until_ready { @@ -224,27 +226,30 @@ pub async fn is_node_up( let retries = FixedInterval::from_millis(IS_NODE_UP_TIME_BETWEEN_CHECKS_MS as u64).take(attempts); - let cli_state = cli_state.clone(); let now = std::time::Instant::now(); + let node_name = node_client.node_name(); + for timeout_duration in retries { - let node_state = cli_state.nodes.get(node_name)?; - // The node is down if it has not stored its default tcp listener in its state file. - if node_state.config().setup().api_transport().is_err() { - trace!(%node_name, "node has not been initialized"); - tokio::time::sleep(timeout_duration).await; - continue; + // The node is down if its default tcp listener has not been started yet + let node = node_client.cli_state().get_node(&node_name).await.ok(); + if let Some(node) = node { + if node.tcp_listener_address().is_none() { + trace!(%node_name, "node has not been initialized"); + tokio::time::sleep(timeout_duration).await; + continue; + } } // Test if node is up // If node is down, we expect it won't reply and the timeout // will trigger the next loop (i.e. no need to sleep here). - let result = node + let result = node_client .set_timeout(timeout_duration) - .tell(ctx, api::query_status()) + .ask::<(), NodeStatus>(ctx, api::query_status()) .await; - if result.is_ok() { + if let Ok(node_status) = result { let elapsed = now.elapsed(); - info!(%node_name, ?elapsed, "node is up"); + info!(%node_name, ?elapsed, "node is up {:?}", node_status); return Ok(true); } else { trace!(%node_name, "node is initializing"); diff --git a/implementations/rust/ockam/ockam_command/src/node/start.rs b/implementations/rust/ockam/ockam_command/src/node/start.rs index 7dc89f565ba..58a0219ece5 100644 --- a/implementations/rust/ockam/ockam_command/src/node/start.rs +++ b/implementations/rust/ockam/ockam_command/src/node/start.rs @@ -1,7 +1,6 @@ use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::nodes::BackgroundNode; use ockam_node::Context; @@ -10,8 +9,6 @@ use crate::node::util::spawn_node; use crate::util::node_rpc; use crate::{docs, fmt_err, fmt_info, fmt_log, fmt_ok, fmt_warn, CommandGlobalOpts, OckamColor}; -use super::get_node_name; - const LONG_ABOUT: &str = include_str!("./static/start/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/start/after_long_help.txt"); @@ -19,9 +16,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/start/after_long_help.txt") /// Start a node that was previously stopped #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct StartCommand { /// Name of the node to be started @@ -42,12 +39,12 @@ async fn run_impl( (opts, cmd): (CommandGlobalOpts, StartCommand), ) -> miette::Result<()> { if cmd.node_name.is_some() || !opts.terminal.can_ask_for_user_input() { - let node_name = &get_node_name(&opts.state, &cmd.node_name); - start_single_node(node_name, opts, &ctx).await?; + let node_name = opts.state.get_node_or_default(&cmd.node_name).await?.name(); + start_single_node(&node_name, opts, &ctx).await?; return Ok(()); } - let inactive_nodes = get_inactive_nodes(&opts)?; + let inactive_nodes = get_inactive_nodes(&opts).await?; match inactive_nodes.len() { 0 => { opts.terminal @@ -104,12 +101,12 @@ async fn start_single_node( mut opts: CommandGlobalOpts, ctx: &Context, ) -> miette::Result<()> { - let node_state = opts.state.nodes.get(node_name)?; + let node_info = opts.state.get_node(node_name).await?; - opts.global_args.verbose = node_state.config().setup().verbose; + opts.global_args.verbose = node_info.verbosity(); // Abort if node is already running - if node_state.is_running() { + if node_info.is_running() { opts.terminal .stdout() .plain(fmt_err!( @@ -121,7 +118,7 @@ async fn start_single_node( } let mut node: BackgroundNode = run_node(node_name, ctx, &opts).await?; - print_query_status(&opts, ctx, node_name, &mut node, true).await?; + print_query_status(&opts, ctx, &mut node, true).await?; Ok(()) } @@ -149,40 +146,44 @@ async fn start_multiple_nodes( Ok(node_starts_output) } -/// Run a single node. Return the BackgroundNode istance of the created node or error +/// Run a single node. Return the BackgroundNode instance of the created node or error async fn run_node( node_name: &str, ctx: &Context, opts: &CommandGlobalOpts, ) -> miette::Result { - let node_state = opts.state.nodes.get(node_name)?; - node_state.kill_process(false)?; - let node_setup = node_state.config().setup(); - let node_name = node_state.name(); + let node_info = opts.state.get_node(node_name).await?; + opts.state.stop_node(node_name, false).await?; + let node_address = node_info + .tcp_listener_address() + .map(|a| a.to_string()) + .unwrap_or("no transport address".to_string()); + // Restart node spawn_node( opts, - node_name, // The selected node name - &node_setup.api_transport()?.addr.to_string(), // The selected node api address - None, // No project information available - None, // No trusted identities - None, // " - None, // " - None, // Launch config - None, // Authority Identity - None, // Credential - None, // Trust Context - None, // Project Name - true, // Restarted nodes will log to files - )?; - - let node = BackgroundNode::create(ctx, &opts.state, node_name).await?; + node_name, // The selected node name + &None, // Use the default identity + &None, // Use the default vault + &node_address, // The selected node api address + None, // No project information available + None, // No trusted identities + None, // " + None, // Launch config + None, // Authority Identity + None, // Credential + None, // Trust Context + true, // Restarted nodes will log to files + ) + .await?; + + let node = BackgroundNode::create_to_node(ctx, &opts.state, node_name).await?; Ok(node) } /// Get a list of the inactive_nodes -fn get_inactive_nodes(opts: &CommandGlobalOpts) -> miette::Result> { - let node_list = opts.state.nodes.list()?; +async fn get_inactive_nodes(opts: &CommandGlobalOpts) -> miette::Result> { + let node_list = opts.state.get_nodes().await?; Ok(node_list .iter() .filter(|node_state| !(node_state.is_running())) diff --git a/implementations/rust/ockam/ockam_command/src/node/stop.rs b/implementations/rust/ockam/ockam_command/src/node/stop.rs index 3fa847aa050..9e318f59508 100644 --- a/implementations/rust/ockam/ockam_command/src/node/stop.rs +++ b/implementations/rust/ockam/ockam_command/src/node/stop.rs @@ -1,9 +1,10 @@ -use crate::node::get_node_name; -use crate::util::local_cmd; -use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::StateDirTrait; + +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/stop/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -12,10 +13,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/stop/after_long_help.txt"); /// Stop a running node #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct StopCommand { /// Name of the node. @@ -27,14 +28,16 @@ pub struct StopCommand { impl StopCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: StopCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_name); - let node_state = opts.state.nodes.get(&node_name)?; - node_state.kill_process(cmd.force)?; +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, StopCommand), +) -> miette::Result<()> { + let node_name = opts.state.get_node_or_default(&cmd.node_name).await?.name(); + opts.state.stop_node(&node_name, cmd.force).await?; opts.terminal .stdout() .plain(fmt_ok!("Stopped node '{}'", &node_name)) diff --git a/implementations/rust/ockam/ockam_command/src/node/util.rs b/implementations/rust/ockam/ockam_command/src/node/util.rs index 9329b03a6cb..79c6e41ad9b 100644 --- a/implementations/rust/ockam/ockam_command/src/node/util.rs +++ b/implementations/rust/ockam/ockam_command/src/node/util.rs @@ -3,11 +3,11 @@ use std::fs::OpenOptions; use std::path::PathBuf; use std::process::{Command, Stdio}; -use miette::Context as _; -use miette::{miette, IntoDiagnostic}; +use miette::IntoDiagnostic; +use miette::{miette, Context as _}; use rand::random; -use ockam_api::cli_state::StateDirTrait; +use ockam_api::cli_state::NamedTrustContext; use ockam_core::env::get_env_with_default; use crate::util::api::TrustContextOpts; @@ -29,12 +29,12 @@ impl Default for NodeManagerDefaults { } } -pub fn delete_all_nodes(opts: &CommandGlobalOpts, force: bool) -> miette::Result<()> { - let nodes_states = opts.state.nodes.list()?; +pub async fn delete_all_nodes(opts: &CommandGlobalOpts, force: bool) -> miette::Result<()> { + let nodes = opts.state.get_nodes().await?; let mut deletion_errors = Vec::new(); - for s in nodes_states { - if let Err(e) = opts.state.nodes.delete_sigkill(s.name(), force) { - deletion_errors.push((s.name().to_string(), e)); + for n in nodes { + if let Err(e) = opts.state.delete_node(&n.name(), force).await { + deletion_errors.push((n.name(), e)); } } if !deletion_errors.is_empty() { @@ -46,28 +46,28 @@ pub fn delete_all_nodes(opts: &CommandGlobalOpts, force: bool) -> miette::Result Ok(()) } -pub fn check_default(opts: &CommandGlobalOpts, name: &str) -> bool { - if let Ok(default) = opts.state.nodes.default() { - return default.name() == name; +pub async fn check_default(opts: &CommandGlobalOpts, name: &str) -> bool { + if let Ok(default_name) = opts.state.get_default_node().await.map(|n| n.name()) { + return default_name == name; } false } /// A utility function to spawn a new node into foreground mode #[allow(clippy::too_many_arguments)] -pub fn spawn_node( +pub async fn spawn_node( opts: &CommandGlobalOpts, name: &str, + identity_name: &Option, + vault_name: &Option, address: &str, - project: Option<&PathBuf>, trusted_identities: Option<&String>, trusted_identities_file: Option<&PathBuf>, reload_from_trusted_identities_file: Option<&PathBuf>, launch_config: Option, - authority_identity: Option<&String>, credential: Option<&String>, - trust_context: Option<&PathBuf>, - project_name: Option<&String>, + trust_context: Option<&NamedTrustContext>, + project_name: Option, logging_to_file: bool, ) -> miette::Result<()> { let mut args = vec![ @@ -87,12 +87,14 @@ pub fn spawn_node( args.push("--no-color".to_string()); } - if let Some(path) = project { - args.push("--project-path".to_string()); - let p = path - .to_str() - .unwrap_or_else(|| panic!("unsupported path {path:?}")); - args.push(p.to_string()) + if let Some(identity_name) = identity_name { + args.push("--identity".to_string()); + args.push(identity_name.to_string()); + } + + if let Some(vault_name) = vault_name { + args.push("--vault".to_string()); + args.push(vault_name.to_string()); } if let Some(l) = launch_config { @@ -119,11 +121,6 @@ pub fn spawn_node( ); } - if let Some(ai) = authority_identity { - args.push("--authority-identity".to_string()); - args.push(ai.to_string()); - } - if let Some(credential) = credential { args.push("--credential".to_string()); args.push(credential.to_string()); @@ -131,12 +128,7 @@ pub fn spawn_node( if let Some(trust_context) = trust_context { args.push("--trust-context".to_string()); - args.push( - trust_context - .to_str() - .unwrap_or_else(|| panic!("unsupported path {trust_context:?}")) - .to_string(), - ); + args.push(trust_context.name()); } if let Some(project_name) = project_name { @@ -146,11 +138,11 @@ pub fn spawn_node( args.push(name.to_owned()); - run_ockam(opts, name, args, logging_to_file) + run_ockam(opts, name, args, logging_to_file).await } /// Run the ockam command line with specific arguments -pub fn run_ockam( +pub async fn run_ockam( opts: &CommandGlobalOpts, node_name: &str, args: Vec, @@ -161,12 +153,16 @@ pub fn run_ockam( // deterministic way of starting a node. let ockam_exe = get_env_with_default("OCKAM", current_exe().unwrap_or_else(|_| "ockam".into())) .into_diagnostic()?; - let node_state = opts.state.nodes.get(node_name)?; let mut cmd = Command::new(ockam_exe); if logging_to_file { - let (mlog, elog) = { (node_state.stdout_log(), node_state.stderr_log()) }; + let (mlog, elog) = { + ( + opts.state.stdout_logs(node_name)?, + opts.state.stderr_logs(node_name)?, + ) + }; let main_log_file = OpenOptions::new() .create(true) .append(true) @@ -182,14 +178,10 @@ pub fn run_ockam( cmd.stdout(main_log_file).stderr(stderr_log_file); } - let child = cmd - .args(args) + cmd.args(args) .stdin(Stdio::null()) .spawn() .into_diagnostic() .context("failed to spawn node")?; - - node_state.set_pid(child.id() as i32)?; - Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/output/output.rs b/implementations/rust/ockam/ockam_command/src/output/output.rs index 2d634ebece6..a1c23f40470 100644 --- a/implementations/rust/ockam/ockam_command/src/output/output.rs +++ b/implementations/rust/ockam/ockam_command/src/output/output.rs @@ -7,14 +7,14 @@ use colorful::Colorful; use miette::miette; use miette::IntoDiagnostic; use minicbor::Encode; +use serde::{Serialize, Serializer}; + use ockam::identity::models::{ CredentialAndPurposeKey, CredentialData, CredentialVerifyingKey, PurposeKeyAttestation, PurposeKeyAttestationData, PurposePublicKey, VersionedData, }; use ockam::identity::{Credential, Identifier, Identity, TimestampInSeconds}; -use serde::{Serialize, Serializer}; - -use ockam_api::cli_state::{ProjectConfigCompact, StateItemTrait, VaultState}; +use ockam_api::cli_state::vaults::NamedVault; use ockam_api::cloud::project::Project; use ockam_api::cloud::space::Space; use ockam_api::nodes::models::portal::{InletStatus, OutletStatus}; @@ -145,7 +145,7 @@ impl Output for Project { write!(w, "Project")?; write!(w, "\n Id: {}", self.id)?; write!(w, "\n Name: {}", self.name)?; - write!(w, "\n Access route: {}", self.access_route)?; + write!(w, "\n Access route: {}", self.access_route()?)?; write!( w, "\n Identity identifier: {}", @@ -179,17 +179,28 @@ Space {}"#, } } +#[derive(Debug, Clone, Serialize)] +pub struct ProjectConfigCompact(pub Project); + impl Output for ProjectConfigCompact { fn output(&self) -> Result { let pi = self - .identity - .as_ref() + .0 + .identifier() .map(|i| i.to_string()) - .unwrap_or_else(|| "N/A".to_string()); - let ar = self.authority_access_route.as_deref().unwrap_or("N/A"); - let ai = self.authority_identity.as_deref().unwrap_or("N/A"); + .unwrap_or_else(|_| "N/A".to_string()); + let ar = self + .0 + .authority_access_route() + .map(|r| r.to_string()) + .unwrap_or_else(|_| "N/A".to_string()); + let ai = self + .0 + .authority_change_history() + .map(|r| r.to_string()) + .unwrap_or_else(|_| "N/A".to_string()); let mut w = String::new(); - writeln!(w, "{}: {}", "Project ID".bold(), self.id)?; + writeln!(w, "{}: {}", "Project ID".bold(), self.0.id())?; writeln!(w, "{}: {}", "Project identity".bold(), pi)?; writeln!(w, "{}: {}", "Authority address".bold(), ar)?; write!(w, "{}: {}", "Authority identity".bold(), ai)?; @@ -361,14 +372,14 @@ From {} to {}"#, } } -impl Output for VaultState { +impl Output for NamedVault { fn output(&self) -> Result { let mut output = String::new(); writeln!(output, "Name: {}", self.name())?; writeln!( output, "Type: {}", - match self.config().is_aws() { + match self.is_kms() { true => "AWS KMS", false => "OCKAM", } diff --git a/implementations/rust/ockam/ockam_command/src/policy/create.rs b/implementations/rust/ockam/ockam_command/src/policy/create.rs index 62a48b35d84..7d7caa7e15d 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/create.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/create.rs @@ -6,9 +6,8 @@ use ockam_api::nodes::models::policy::Policy; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::policy::policy_path; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::CommandGlobalOpts; #[derive(Clone, Debug, Args)] @@ -41,11 +40,9 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.at).await?; let bdy = Policy::new(cmd.expression); let req = Request::post(policy_path(&cmd.resource, &cmd.action)).body(bdy); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; node.tell(ctx, req).await?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/policy/delete.rs b/implementations/rust/ockam/ockam_command/src/policy/delete.rs index e25d2512aa4..5b9d721ceb9 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/delete.rs @@ -6,9 +6,8 @@ use ockam_abac::{Action, Resource}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::policy::policy_path; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{fmt_ok, CommandGlobalOpts}; #[derive(Clone, Debug, Args)] @@ -42,15 +41,13 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: DeleteCommand, ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; if opts .terminal .confirmed_with_flag_or_prompt(cmd.yes, "Are you sure you want to delete this policy?")? { + let node = BackgroundNode::create(ctx, &opts.state, &cmd.at).await?; let policy_path = policy_path(&cmd.resource, &cmd.action); let req = Request::delete(&policy_path); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; node.tell(ctx, req).await?; opts.terminal @@ -63,7 +60,7 @@ async fn run_impl( .json(serde_json::json!({ "resource": &cmd.resource.to_string(), "action": &cmd.action.to_string(), - "at": &node_name} + "at": &node.node_name()} )) .write_line()?; } diff --git a/implementations/rust/ockam/ockam_command/src/policy/list.rs b/implementations/rust/ockam/ockam_command/src/policy/list.rs index 8722351e4bd..f981279ffbf 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/list.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/list.rs @@ -2,21 +2,18 @@ use std::fmt::Write; use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; use ockam_abac::Resource; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::policy::{Expression, PolicyList}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::output::Output; use crate::terminal::OckamColor; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{CommandGlobalOpts, Result}; #[derive(Clone, Debug, Args)] @@ -39,18 +36,10 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> miette::Result<()> { - let resource = cmd.resource; - - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", &node_name)); - } - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.at).await?; let is_finished: Mutex = Mutex::new(false); + let resource = cmd.resource; let get_policies = async { let req = Request::get(format!("/policy/{resource}")); let policies: PolicyList = node.ask(ctx, req).await?; @@ -59,7 +48,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let output_messages = vec![format!( "Listing Policies on {} for Resource {}...\n", - node_name + node.node_name() .to_string() .color(OckamColor::PrimaryResource.color()), resource @@ -75,8 +64,8 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let list = opts.terminal.build_list( policies.expressions(), - &format!("Policies on Node {} for {}", &node_name, resource), - &format!("No Policies on Node {} for {}", &node_name, resource), + &format!("Policies on Node {} for {}", &node.node_name(), resource), + &format!("No Policies on Node {} for {}", &node.node_name(), resource), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/policy/mod.rs b/implementations/rust/ockam/ockam_command/src/policy/mod.rs index 2922815c5b8..518c7ba2d9a 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/mod.rs @@ -3,9 +3,9 @@ use clap::{Args, Subcommand}; use ockam::Context; use ockam_abac::expr::{eq, ident, str}; use ockam_abac::{Action, Resource}; +use ockam_api::nodes::models::policy::Policy; use ockam_api::nodes::models::policy::PolicyList; use ockam_api::nodes::BackgroundNode; -use ockam_api::{config::lookup::ProjectLookup, nodes::models::policy::Policy}; use ockam_core::api::Request; use crate::policy::create::CreateCommand; @@ -55,8 +55,8 @@ pub(crate) async fn has_policy( opts: &CommandGlobalOpts, resource: &Resource, ) -> Result { + let node = BackgroundNode::create_to_node(ctx, &opts.state, node_name).await?; let req = Request::get(format!("/policy/{resource}")); - let node = BackgroundNode::create(ctx, &opts.state, node_name).await?; let policies: PolicyList = node.ask(ctx, req).await?; Ok(!policies.expressions().is_empty()) } @@ -65,17 +65,15 @@ pub(crate) async fn add_default_project_policy( node_name: &str, ctx: &Context, opts: &CommandGlobalOpts, - project: ProjectLookup, + project_id: String, resource: &Resource, ) -> miette::Result<()> { - let expr = eq([ - ident("subject.trust_context_id"), - str(project.id.to_string()), - ]); + let node = BackgroundNode::create_to_node(ctx, &opts.state, node_name).await?; + + let expr = eq([ident("subject.trust_context_id"), str(project_id)]); let bdy = Policy::new(expr); let req = Request::post(policy_path(resource, &Action::new("handle_message"))).body(bdy); - let node = BackgroundNode::create(ctx, &opts.state, node_name).await?; node.tell(ctx, req).await?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/policy/show.rs b/implementations/rust/ockam/ockam_command/src/policy/show.rs index 0c0eb1ea592..84ee62f5d16 100644 --- a/implementations/rust/ockam/ockam_command/src/policy/show.rs +++ b/implementations/rust/ockam/ockam_command/src/policy/show.rs @@ -2,7 +2,6 @@ use clap::Args; use ockam::Context; use ockam_abac::{Action, Resource}; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::policy::Policy; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; @@ -34,9 +33,8 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ShowCommand) -> miette::Result<()> { - let node_name = extract_address_value(&cmd.at)?; + let node = BackgroundNode::create_to_node(ctx, &opts.state, &cmd.at).await?; let req = Request::get(policy_path(&cmd.resource, &cmd.action)); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; let policy: Policy = node.ask(ctx, req).await?; println!("{}", policy.expression()); Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/configure_confluent.rs b/implementations/rust/ockam/ockam_command/src/project/addon/configure_confluent.rs index 07111e3d476..f920b113a75 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/configure_confluent.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/configure_confluent.rs @@ -6,7 +6,7 @@ use ockam::Context; use ockam_api::cloud::addon::{Addons, ConfluentConfig}; use ockam_api::nodes::InMemoryNode; -use crate::project::addon::{check_configuration_completion, get_project_id}; +use crate::project::addon::check_configuration_completion; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; @@ -54,16 +54,16 @@ async fn run_impl( project_name, bootstrap_server, } = cmd; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let config = ConfluentConfig::new(bootstrap_server); let node = InMemoryNode::start(&ctx, &opts.state).await?; let controller = node.create_controller().await?; let response = controller - .configure_confluent_addon(&ctx, project_id.clone(), config) + .configure_confluent_addon(&ctx, project_id, config) .await?; - check_configuration_completion(&opts, &ctx, &node, project_id, response.operation_id).await?; + check_configuration_completion(&opts, &ctx, &node, project_id, &response.operation_id).await?; opts.terminal .write_line(&fmt_ok!("Confluent addon configured successfully"))?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/configure_influxdb.rs b/implementations/rust/ockam/ockam_command/src/project/addon/configure_influxdb.rs index c853ad1fa73..ef6d7a69b19 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/configure_influxdb.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/configure_influxdb.rs @@ -10,7 +10,7 @@ use ockam_api::cloud::addon::Addons; use ockam_api::cloud::project::InfluxDBTokenLeaseManagerConfig; use ockam_api::nodes::InMemoryNode; -use crate::project::addon::{check_configuration_completion, get_project_id}; +use crate::project::addon::check_configuration_completion; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; @@ -133,7 +133,7 @@ async fn run_impl( user_access_role, admin_access_role, } = cmd; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let perms = match (permissions, permissions_path) { (_, Some(p)) => std::fs::read_to_string(p).into_diagnostic()?, @@ -159,9 +159,9 @@ async fn run_impl( let controller = node.create_controller().await?; let response = controller - .configure_influxdb_addon(&ctx, project_id.clone(), config) + .configure_influxdb_addon(&ctx, project_id, config) .await?; - check_configuration_completion(&opts, &ctx, &node, project_id, response.operation_id).await?; + check_configuration_completion(&opts, &ctx, &node, project_id, &response.operation_id).await?; opts.terminal .write_line(&fmt_ok!("InfluxDB addon configured successfully"))?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/configure_okta.rs b/implementations/rust/ockam/ockam_command/src/project/addon/configure_okta.rs index e0b23a31f46..b2d0710b52a 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/configure_okta.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/configure_okta.rs @@ -16,7 +16,7 @@ use ockam_api::enroll::okta_oidc_provider::OktaOidcProvider; use ockam_api::minicbor_url::Url; use ockam_api::nodes::InMemoryNode; -use crate::project::addon::{check_configuration_completion, get_project_id}; +use crate::project::addon::check_configuration_completion; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts, Result}; @@ -96,7 +96,7 @@ async fn run_impl( client_id, attributes, } = cmd; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let base_url = Url::parse(tenant.as_str()) .into_diagnostic() @@ -122,9 +122,9 @@ async fn run_impl( let controller = node.create_controller().await?; let response = controller - .configure_okta_addon(&ctx, project_id.clone(), okta_config) + .configure_okta_addon(&ctx, project_id, okta_config) .await?; - check_configuration_completion(&opts, &ctx, &node, project_id, response.operation_id).await?; + check_configuration_completion(&opts, &ctx, &node, project_id, &response.operation_id).await?; opts.terminal .write_line(&fmt_ok!("Okta addon configured successfully"))?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/disable.rs b/implementations/rust/ockam/ockam_command/src/project/addon/disable.rs index 28a51ecc554..183b363c185 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/disable.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/disable.rs @@ -7,7 +7,6 @@ use ockam_api::cloud::addon::Addons; use ockam_api::nodes::InMemoryNode; use crate::operation::util::check_for_completion; -use crate::project::addon::get_project_id; use crate::util::node_rpc; use crate::{fmt_ok, CommandGlobalOpts}; @@ -47,11 +46,13 @@ async fn run_impl( project_name, addon_id, } = cmd; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let node = InMemoryNode::start(&ctx, &opts.state).await?; let controller = node.create_controller().await?; - let response = controller.disable_addon(&ctx, project_id, addon_id).await?; + let response = controller + .disable_addon(&ctx, project_id, &addon_id) + .await?; let operation_id = response.operation_id; check_for_completion(&opts, &ctx, &controller, &operation_id).await?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/list.rs b/implementations/rust/ockam/ockam_command/src/project/addon/list.rs index 4a4823f9567..ab631282d21 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/list.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/list.rs @@ -5,7 +5,6 @@ use ockam::Context; use ockam_api::cloud::addon::Addons; use ockam_api::nodes::InMemoryNode; -use crate::project::addon::get_project_id; use crate::util::node_rpc; use crate::CommandGlobalOpts; @@ -33,7 +32,7 @@ async fn run_impl( (opts, cmd): (CommandGlobalOpts, AddonListSubcommand), ) -> miette::Result<()> { let project_name = cmd.project_name; - let project_id = get_project_id(&opts.state, project_name.as_str())?; + let project_id = &opts.state.get_project_by_name(&project_name).await?.id(); let node = InMemoryNode::start(&ctx, &opts.state).await?; let controller = node.create_controller().await?; diff --git a/implementations/rust/ockam/ockam_command/src/project/addon/mod.rs b/implementations/rust/ockam/ockam_command/src/project/addon/mod.rs index 31b78894e2c..fa0dc172184 100644 --- a/implementations/rust/ockam/ockam_command/src/project/addon/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/project/addon/mod.rs @@ -1,34 +1,28 @@ -mod configure_confluent; -mod configure_influxdb; -mod configure_okta; -mod disable; -mod list; - use core::fmt::Write; use clap::{Args, Subcommand}; -use miette::Context as _; -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; use ockam_api::cloud::addon::Addon; -use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; - use ockam_node::Context; +use crate::operation::util::check_for_completion; +use crate::output::Output; use crate::project::addon::configure_confluent::AddonConfigureConfluentSubcommand; use crate::project::addon::configure_influxdb::AddonConfigureInfluxdbSubcommand; use crate::project::addon::configure_okta::AddonConfigureOktaSubcommand; use crate::project::addon::disable::AddonDisableSubcommand; use crate::project::addon::list::AddonListSubcommand; - -use crate::output::Output; -use crate::util::api::CloudOpts; - -use crate::operation::util::check_for_completion; use crate::project::util::check_project_readiness; +use crate::util::api::CloudOpts; use crate::{CommandGlobalOpts, Result}; +mod configure_confluent; +mod configure_influxdb; +mod configure_okta; +mod disable; +mod list; + /// Manage addons for a project #[derive(Clone, Debug, Args)] #[command(arg_required_else_help = true, subcommand_required = true)] @@ -103,27 +97,15 @@ impl Output for Vec { } } -pub fn get_project_id(cli_state: &CliState, project_name: &str) -> Result { - Ok(cli_state - .projects - .get(project_name) - .context(format!( - "Failed to get project {project_name} from config lookup" - ))? - .config() - .id - .clone()) -} - async fn check_configuration_completion( opts: &CommandGlobalOpts, ctx: &Context, node: &InMemoryNode, - project_id: String, - operation_id: String, + project_id: &str, + operation_id: &str, ) -> Result<()> { let controller = node.create_controller().await?; - check_for_completion(opts, ctx, &controller, &operation_id).await?; + check_for_completion(opts, ctx, &controller, operation_id).await?; let project = controller.get_project(ctx, project_id).await?; let _ = check_project_readiness(opts, ctx, node, project).await?; Ok(()) diff --git a/implementations/rust/ockam/ockam_command/src/project/create.rs b/implementations/rust/ockam/ockam_command/src/project/create.rs index 1a24db57fd7..e180256b4a0 100644 --- a/implementations/rust/ockam/ockam_command/src/project/create.rs +++ b/implementations/rust/ockam/ockam_command/src/project/create.rs @@ -2,14 +2,14 @@ use clap::Args; use ockam::Context; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; use crate::operation::util::check_for_completion; use crate::project::util::check_project_readiness; use crate::util::api::CloudOpts; -use crate::util::{api, node_rpc}; +use crate::util::node_rpc; +use crate::util::parsers::validate_project_name; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -50,33 +50,14 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - let space_id = opts.state.spaces.get(&cmd.space_name)?.config().id.clone(); let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - - let project = controller - .create_project(ctx, space_id, cmd.project_name, vec![]) + let project = node + .create_project(ctx, &cmd.space_name, &cmd.project_name, vec![]) .await?; let operation_id = project.operation_id.clone().unwrap(); + let controller = node.create_controller().await?; check_for_completion(&opts, ctx, &controller, &operation_id).await?; let project = check_project_readiness(&opts, ctx, &node, project).await?; - opts.state - .projects - .overwrite(&project.name, project.clone())?; - opts.state - .trust_contexts - .overwrite(&project.name, project.clone().try_into()?)?; opts.println(&project)?; Ok(()) } - -fn validate_project_name(s: &str) -> Result { - match api::validate_cloud_resource_name(s) { - Ok(_) => Ok(s.to_string()), - Err(_e)=> Err(String::from( - "project name can contain only alphanumeric characters and the '-', '_' and '.' separators. \ - Separators must occur between alphanumeric characters. This implies that separators can't \ - occur at the start or end of the name, nor they can occur in sequence.", - )), - } -} diff --git a/implementations/rust/ockam/ockam_command/src/project/delete.rs b/implementations/rust/ockam/ockam_command/src/project/delete.rs index 5bb6c42c5b4..128a7358108 100644 --- a/implementations/rust/ockam/ockam_command/src/project/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/project/delete.rs @@ -2,12 +2,10 @@ use clap::Args; use colorful::Colorful; use ockam::Context; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; -use crate::project::util::refresh_projects; use crate::util::api::CloudOpts; use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; @@ -57,32 +55,9 @@ async fn run_impl( .terminal .confirmed_with_flag_or_prompt(cmd.yes, "Are you sure you want to delete this project?")? { - let space_id = opts.state.spaces.get(&cmd.space_name)?.config().id.clone(); let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - - // Lookup project - let project_id = match opts.state.projects.get(&cmd.project_name) { - Ok(state) => state.config().id.clone(), - Err(_) => { - // The project is not in the config file. - // Fetch all available projects from the cloud. - refresh_projects(&opts, ctx, &controller).await?; - - // If the project is not found in the lookup, then it must not exist in the cloud, so we exit the command. - match opts.state.projects.get(&cmd.project_name) { - Ok(state) => state.config().id.clone(), - Err(_) => { - return Ok(()); - } - } - } - }; - - // Send request - controller.delete_project(ctx, space_id, project_id).await?; - - opts.state.projects.delete(&cmd.project_name)?; + node.delete_project_by_name(ctx, &cmd.space_name, &cmd.project_name) + .await?; opts.terminal .stdout() .plain(fmt_ok!( diff --git a/implementations/rust/ockam/ockam_command/src/project/enroll.rs b/implementations/rust/ockam/ockam_command/src/project/enroll.rs index aa6783f9258..ca13863831a 100644 --- a/implementations/rust/ockam/ockam_command/src/project/enroll.rs +++ b/implementations/rust/ockam/ockam_command/src/project/enroll.rs @@ -5,18 +5,15 @@ use miette::Context as _; use miette::{miette, IntoDiagnostic}; use ockam::Context; -use ockam_api::cli_state::{ProjectConfigCompact, StateDirTrait, StateItemTrait}; +use ockam_api::cli_state::enrollments::EnrollmentTicket; use ockam_api::cloud::project::{OktaAuth0, Project}; use ockam_api::cloud::AuthorityNode; use ockam_api::enroll::enrollment::Enrollment; use ockam_api::enroll::oidc_service::OidcService; use ockam_api::enroll::okta_oidc_provider::OktaOidcProvider; -use ockam_api::identity::EnrollmentTicket; use ockam_api::nodes::InMemoryNode; use crate::enroll::OidcServiceExt; -use crate::identity::{get_identity_name, initialize_identity_if_default}; - use crate::output::CredentialAndPurposeKeyDisplay; use crate::util::api::{CloudOpts, TrustContextOpts}; use crate::util::node_rpc; @@ -69,7 +66,6 @@ pub fn parse_enroll_ticket(hex_encoded_data_or_path: &str) -> Result miette::Result { let project = retrieve_project(opts, &cmd).await?; - let project_authority = project - .authority() - .await - .into_diagnostic()? - .ok_or_else(|| miette!("Authority details not configured"))?; - let identity_name = get_identity_name(&opts.state, &cmd.cloud_opts.identity); + let identity_name = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; // Create secure channel to the project's authority node - let trust_context_config = cmd.trust_opts.to_config(&opts.state)?.build(); + let trust_context = opts + .state + .retrieve_trust_context( + &cmd.trust_opts.trust_context, + &cmd.trust_opts.project_name, + &None, + &None, + ) + .await?; let node = InMemoryNode::start_with_trust_context( ctx, &opts.state, - cmd.trust_opts.project_path.as_ref(), - trust_context_config, + cmd.trust_opts.project_name, + trust_context, ) .await?; let authority_node: AuthorityNode = node .create_authority_client( - project_authority.identity_id(), - project_authority.address(), + &project.authority_identifier().await.into_diagnostic()?, + &project.authority_access_route().into_diagnostic()?, Some(identity_name), ) .await?; @@ -140,41 +142,25 @@ pub async fn project_enroll( } async fn retrieve_project(opts: &CommandGlobalOpts, cmd: &EnrollCommand) -> Result { - let project_as_string: String; - // Retrieve project info from the enrollment ticket or project.json in the case of okta auth - let proj: ProjectConfigCompact = if let Some(ticket) = &cmd.enroll_ticket { + let project = if let Some(ticket) = &cmd.enroll_ticket { ticket .project .as_ref() .expect("Enrollment ticket is invalid. Ticket does not contain a project.") .clone() - .try_into()? } else { // OKTA AUTHENTICATION FLOW | PREVIOUSLY ENROLLED FLOW // currently okta auth does not use an enrollment token // however, it could be worked to use one in the future // // REQUIRES Project passed or default project - let path = match cmd.trust_opts.project_path.as_ref() { - Some(p) => p.clone(), - None => { - let default_project = opts - .state - .projects - .default() - .context("A default project or project parameter is required.")?; - default_project.path().clone() - } - }; - - // Read (okta and authority) project parameters from project.json - project_as_string = tokio::fs::read_to_string(path).await.into_diagnostic()?; - serde_json::from_str(&project_as_string).into_diagnostic()? + opts.state + .get_project_by_name_or_default(&cmd.trust_opts.project_name) + .await + .context("A default project or project parameter is required.")? }; - let project: Project = (&proj).into(); - let trust_context_name = if let Some(trust_context_name) = &cmd.new_trust_context_name { trust_context_name } else { @@ -182,8 +168,8 @@ async fn retrieve_project(opts: &CommandGlobalOpts, cmd: &EnrollCommand) -> Resu }; if !cmd.force { - if let Ok(trust_context) = opts.state.trust_contexts.get(trust_context_name) { - if trust_context.config().id() != project.id { + if let Ok(trust_context) = opts.state.get_trust_context(trust_context_name).await { + if trust_context.trust_context_id() != project.id { return Err(miette!( "A trust context with the name {} already exists and is associated with a different project. Please choose a different name.", trust_context_name @@ -193,12 +179,14 @@ async fn retrieve_project(opts: &CommandGlobalOpts, cmd: &EnrollCommand) -> Resu } opts.state - .projects - .overwrite(&project.name, project.clone())?; - - opts.state - .trust_contexts - .overwrite(trust_context_name, project.clone().try_into()?)?; + .create_trust_context( + Some(trust_context_name.clone()), + Some(project.id()), + None, + project.authority_identity().await.ok(), + project.authority_access_route().ok(), + ) + .await?; Ok(project) } diff --git a/implementations/rust/ockam/ockam_command/src/project/import.rs b/implementations/rust/ockam/ockam_command/src/project/import.rs new file mode 100644 index 00000000000..fff74befe5e --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/project/import.rs @@ -0,0 +1,95 @@ +use clap::Args; +use colorful::Colorful; + +use ockam::identity::{Identifier, Identity}; +use ockam::Context; +use ockam_multiaddr::MultiAddr; + +use crate::util::node_rpc; +use crate::util::parsers::{ + identity_identifier_parser, identity_parser, multiaddr_parser, validate_project_name, +}; +use crate::{docs, fmt_err, fmt_ok, CommandGlobalOpts}; + +const LONG_ABOUT: &str = include_str!("./static/import/long_about.txt"); +const AFTER_LONG_HELP: &str = include_str!("./static/import/after_long_help.txt"); + +/// Import projects +#[derive(Clone, Debug, Args)] +#[command( +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), +)] +pub struct ImportCommand { + /// Project name + #[arg(long, value_parser = validate_project_name)] + pub project_name: String, + + /// Project id + #[arg(long)] + pub project_id: String, + + /// Project identifier + #[arg(long, value_name = "IDENTIFIER", value_parser = identity_identifier_parser)] + pub project_identifier: Option, + + /// Project access route + #[arg(long, value_name = "MULTIADDR", value_parser = multiaddr_parser)] + pub project_access_route: MultiAddr, + + /// Authority identity + #[arg(long, value_name = "IDENTITY", value_parser = identity_parser)] + pub authority_identity: Option, + + /// Authority access route + #[arg(long, value_name = "MULTIADDR", value_parser = multiaddr_parser)] + pub authority_access_route: Option, +} + +impl ImportCommand { + pub fn run(self, options: CommandGlobalOpts) { + node_rpc(rpc, (options, self)); + } +} + +async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ImportCommand)) -> miette::Result<()> { + run_impl(&ctx, opts, cmd).await +} + +async fn run_impl( + _ctx: &Context, + opts: CommandGlobalOpts, + cmd: ImportCommand, +) -> miette::Result<()> { + match opts + .state + .import_project( + &cmd.project_id, + &cmd.project_name, + &cmd.project_identifier, + &cmd.project_access_route, + &cmd.authority_identity, + &cmd.authority_access_route, + ) + .await + { + Ok(_) => opts + .terminal + .stdout() + .plain(fmt_ok!( + "Successfully imported project {}", + &cmd.project_name + )) + .write_line()?, + Err(e) => opts + .terminal + .stdout() + .plain(fmt_err!( + "The project {} could not be imported: {}", + &cmd.project_name, + e.to_string() + )) + .write_line()?, + }; + Ok(()) +} diff --git a/implementations/rust/ockam/ockam_command/src/project/info.rs b/implementations/rust/ockam/ockam_command/src/project/info.rs index 375e0d21074..d943e5edb95 100644 --- a/implementations/rust/ockam/ockam_command/src/project/info.rs +++ b/implementations/rust/ockam/ockam_command/src/project/info.rs @@ -2,13 +2,11 @@ use clap::Args; use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cli_state::{ProjectConfigCompact, StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; -use crate::output::Output; -use crate::project::util::refresh_projects; +use crate::output::{Output, ProjectConfigCompact}; use crate::util::api::CloudOpts; use crate::util::node_rpc; use crate::CommandGlobalOpts; @@ -38,19 +36,8 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, InfoCommand)) -> mie async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: InfoCommand) -> miette::Result<()> { let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - - // Lookup project - let id = match opts.state.projects.get(&cmd.name) { - Ok(state) => state.config().id.clone(), - Err(_) => { - refresh_projects(&opts, ctx, &controller).await?; - opts.state.projects.get(&cmd.name)?.config().id.clone() - } - }; - - let project = controller.get_project(ctx, id).await?; - let info: ProjectConfigCompact = project.into(); + let project = node.get_project_by_name(ctx, &cmd.name).await?; + let info = ProjectConfigCompact(project); opts.terminal .stdout() .plain(info.output()?) diff --git a/implementations/rust/ockam/ockam_command/src/project/list.rs b/implementations/rust/ockam/ockam_command/src/project/list.rs index 550dee8a70b..3bbc2c604d3 100644 --- a/implementations/rust/ockam/ockam_command/src/project/list.rs +++ b/implementations/rust/ockam/ockam_command/src/project/list.rs @@ -4,9 +4,7 @@ use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::cloud::project::Projects; - use ockam_api::nodes::InMemoryNode; use crate::util::api::CloudOpts; @@ -20,9 +18,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List projects #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ListCommand { #[command(flatten)] @@ -40,15 +38,14 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, _cmd: ListCommand) -> miette::Result<()> { - if !opts.state.is_enrolled()? { + if !opts.state.is_enrolled().await? { return Err(miette!("You must enroll before you can list your projects")); } let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; let is_finished: Mutex = Mutex::new(false); let get_projects = async { - let projects = controller.list_projects(ctx).await?; + let projects = node.get_projects(ctx).await?; *is_finished.lock().await = true; Ok(projects) }; @@ -66,12 +63,6 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, _cmd: ListCommand) -> .build_list(&projects, "Projects", "No projects found on this system.")?; let json = serde_json::to_string_pretty(&projects).into_diagnostic()?; - for project in &projects { - opts.state - .projects - .overwrite(&project.name, project.clone())?; - } - opts.terminal .stdout() .plain(plain) diff --git a/implementations/rust/ockam/ockam_command/src/project/mod.rs b/implementations/rust/ockam/ockam_command/src/project/mod.rs index 25a311deeaa..8ec34266515 100644 --- a/implementations/rust/ockam/ockam_command/src/project/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/project/mod.rs @@ -1,38 +1,40 @@ -mod addon; -mod create; -mod delete; -pub(crate) mod enroll; -mod info; -mod list; -mod show; -mod ticket; -pub mod util; -mod version; - +use crate::docs; use clap::{Args, Subcommand}; -pub use crate::credential::get::GetCommand; pub use addon::AddonCommand; pub use create::CreateCommand; pub use delete::DeleteCommand; pub use enroll::EnrollCommand; +pub use import::ImportCommand; pub use info::InfoCommand; pub use list::ListCommand; pub use show::ShowCommand; pub use ticket::TicketCommand; pub use version::VersionCommand; -use crate::docs; +pub use crate::credential::get::GetCommand; use crate::CommandGlobalOpts; +mod addon; +mod create; +mod delete; +pub(crate) mod enroll; +mod import; +mod info; +mod list; +mod show; +mod ticket; +pub mod util; +mod version; + const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); /// Manage Projects in Ockam Orchestrator #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - subcommand_required = true, - long_about = docs::about(LONG_ABOUT), +arg_required_else_help = true, +subcommand_required = true, +long_about = docs::about(LONG_ABOUT), )] pub struct ProjectCommand { #[command(subcommand)] @@ -42,6 +44,7 @@ pub struct ProjectCommand { #[derive(Clone, Debug, Subcommand)] pub enum ProjectSubcommand { Create(CreateCommand), + Import(ImportCommand), Delete(DeleteCommand), List(ListCommand), Show(ShowCommand), @@ -49,13 +52,14 @@ pub enum ProjectSubcommand { Information(InfoCommand), Ticket(TicketCommand), Addon(AddonCommand), - Enroll(EnrollCommand), + Enroll(Box), } impl ProjectCommand { pub fn run(self, options: CommandGlobalOpts) { match self.subcommand { ProjectSubcommand::Create(c) => c.run(options), + ProjectSubcommand::Import(c) => c.run(options), ProjectSubcommand::Delete(c) => c.run(options), ProjectSubcommand::List(c) => c.run(options), ProjectSubcommand::Show(c) => c.run(options), diff --git a/implementations/rust/ockam/ockam_command/src/project/show.rs b/implementations/rust/ockam/ockam_command/src/project/show.rs index b6a98330301..652ddaf2e37 100644 --- a/implementations/rust/ockam/ockam_command/src/project/show.rs +++ b/implementations/rust/ockam/ockam_command/src/project/show.rs @@ -1,25 +1,21 @@ use clap::Args; use miette::IntoDiagnostic; -use ockam_api::cloud::Controller; use crate::terminal::tui::ShowCommandTui; use ockam::Context; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::cloud::project::{Project, Projects}; use tokio::try_join; use ockam_api::nodes::InMemoryNode; -use crate::output::Output; +use crate::output::{Output, ProjectConfigCompact}; use crate::util::api::CloudOpts; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; use tokio::sync::Mutex; -use super::util::refresh_projects; - const LONG_ABOUT: &str = include_str!("./static/show/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); @@ -27,9 +23,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show projects #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ShowCommand { /// Name of the project. @@ -58,7 +54,7 @@ pub struct ProjectShowTui { ctx: Context, opts: CommandGlobalOpts, project_name: Option, - controller: Controller, + node: InMemoryNode, } impl ProjectShowTui { pub async fn run( @@ -67,14 +63,11 @@ impl ProjectShowTui { project_name: Option, ) -> miette::Result<()> { let node = InMemoryNode::start(&ctx, &opts.state).await?; - let controller = node.create_controller().await?; - //Refrsh the project list - refresh_projects(&opts, &ctx, &controller).await?; let tui = Self { ctx, opts, project_name, - controller, + node, }; tui.show().await } @@ -91,31 +84,32 @@ impl ShowCommandTui for ProjectShowTui { self.opts.terminal.clone() } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.projects.list_items_names()?) + Ok(self + .opts + .state + .get_projects() + .await? + .iter() + .map(|p| p.name()) + .collect()) } async fn get_arg_item_name_or_default(&self) -> miette::Result { let project = match self.cmd_arg_item_name() { Some(command) => command.to_owned(), - None => self.opts.state.projects.default()?.name().to_owned(), + None => self.opts.state.get_default_project().await?.name(), }; Ok(project) } async fn show_single(&self, item_name: &str) -> miette::Result<()> { - let project_state = &self.opts.state.projects.get(item_name)?; - let id = project_state.config().id.clone(); - let project = self.controller.get_project(&self.ctx, id).await?; + let project = self.node.get_project_by_name(&self.ctx, item_name).await?; + let project_output = ProjectConfigCompact(project); self.terminal() .stdout() - .plain(project.output()?) - .json(serde_json::to_string_pretty(&project).into_diagnostic()?) + .plain(project_output.output()?) + .json(serde_json::to_string_pretty(&project_output).into_diagnostic()?) .write_line()?; - - self.opts - .state - .projects - .overwrite(&project.name, project.clone())?; Ok(()) } async fn show_multiple(&self, selected_items_names: Vec) -> miette::Result<()> { @@ -124,9 +118,10 @@ impl ShowCommandTui for ProjectShowTui { let mut projects_list: Vec = Vec::with_capacity(selected_items_names.len()); let get_projects = async { for project_name in selected_items_names.iter() { - let project_state = &self.opts.state.projects.get(project_name)?; - let id = project_state.config().id.clone(); - let project = self.controller.get_project(&self.ctx, id).await?; + let project = self + .node + .get_project_by_name(&self.ctx, project_name) + .await?; projects_list.push(project) } *is_finished.lock().await = true; @@ -143,14 +138,11 @@ impl ShowCommandTui for ProjectShowTui { "Projects", "No projects found on this system.", )?; - let json = serde_json::to_string_pretty(&projects).into_diagnostic()?; - + let mut project_outputs = vec![]; for project in projects { - self.opts - .state - .projects - .overwrite(&project.name, project.clone())?; + project_outputs.push(ProjectConfigCompact(project)); } + let json = serde_json::to_string_pretty(&project_outputs).into_diagnostic()?; self.terminal() .stdout() diff --git a/implementations/rust/ockam/ockam_command/src/project/static/import/after_long_help.txt b/implementations/rust/ockam/ockam_command/src/project/static/import/after_long_help.txt new file mode 100644 index 00000000000..f5badf53a93 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/project/static/import/after_long_help.txt @@ -0,0 +1,10 @@ +```sh +# To import a project +$ ockam project import + --project-name name \\ + --project-id 12345 \\ + --project-identifier I1234561234561234561234561234561234561234 \\ + --project-access-route /dnsaddr/127.0.0.1/tcp/4000/service/api \\ + --authority-identity I1234561234561234561234561234561234561234 \\ + --authority-access-route /dnsaddr/127.0.0.1/tcp/5000/service/api +``` diff --git a/implementations/rust/ockam/ockam_command/src/project/static/import/long_about.txt b/implementations/rust/ockam/ockam_command/src/project/static/import/long_about.txt new file mode 100644 index 00000000000..c3389fdb523 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/project/static/import/long_about.txt @@ -0,0 +1 @@ +This command will import a project in the local database. If the project already exists, an error is returned diff --git a/implementations/rust/ockam/ockam_command/src/project/ticket.rs b/implementations/rust/ockam/ockam_command/src/project/ticket.rs index 2617fdbae2b..b2cb40c0eaf 100644 --- a/implementations/rust/ockam/ockam_command/src/project/ticket.rs +++ b/implementations/rust/ockam/ockam_command/src/project/ticket.rs @@ -1,23 +1,20 @@ -use crate::util::duration::duration_parser; -use clap::Args; -use ockam_api::config::cli::TrustContextConfig; -use ockam_api::identity::EnrollmentTicket; use std::collections::HashMap; use std::time::Duration; +use clap::Args; use miette::{miette, IntoDiagnostic}; + use ockam::identity::Identifier; use ockam::Context; use ockam_api::authenticator::enrollment_tokens::{Members, TokenIssuer}; -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; -use ockam_api::config::lookup::{ProjectAuthority, ProjectLookup}; +use ockam_api::cli_state::enrollments::EnrollmentTicket; +use ockam_api::cli_state::CliState; +use ockam_api::cloud::project::Project; use ockam_api::nodes::InMemoryNode; - use ockam_multiaddr::{proto, MultiAddr, Protocol}; -use crate::identity::{get_identity_name, initialize_identity_if_default}; - use crate::util::api::{CloudOpts, TrustContextOpts}; +use crate::util::duration::duration_parser; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts, Result}; @@ -27,8 +24,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/ticket/after_long_help.txt" /// Add members to a project as an authorised enroller. #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP), +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct TicketCommand { /// Orchestrator address to resolve projects present in the `at` argument @@ -48,7 +45,7 @@ pub struct TicketCommand { #[arg(short, long = "attribute", value_name = "ATTRIBUTE")] attributes: Vec, - #[arg(long = "expires-in", value_name = "DURATION", conflicts_with = "member", value_parser=duration_parser)] + #[arg(long = "expires-in", value_name = "DURATION", conflicts_with = "member", value_parser = duration_parser)] expires_in: Option, #[arg( @@ -61,7 +58,6 @@ pub struct TicketCommand { impl TicketCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.cloud_opts.identity); node_rpc(run_impl, (opts, self)); } @@ -81,51 +77,60 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, TicketCommand), ) -> miette::Result<()> { - let trust_context_config = cmd.trust_opts.to_config(&opts.state)?.build(); + let trust_context = opts + .state + .retrieve_trust_context( + &cmd.trust_opts.trust_context, + &cmd.trust_opts.project_name, + &None, + &None, + ) + .await?; let node = InMemoryNode::start_with_trust_context( &ctx, &opts.state, - cmd.trust_opts.project_path.as_ref(), - trust_context_config, + cmd.trust_opts.project_name.clone(), + trust_context, ) .await?; - let mut project: Option = None; - let mut trust_context: Option = None; + let mut project: Option = None; - let authority_node = if let Some(tc) = cmd.trust_opts.trust_context.as_ref() { - let tc = &opts.state.trust_contexts.read_config_from_path(tc)?; - trust_context = Some(tc.clone()); - let cred_retr = tc + let authority_node = if let Some(name) = cmd.trust_opts.trust_context.as_ref() { + let authority = if let Some(authority) = opts + .state + .get_trust_context(name) + .await? .authority() + .await .into_diagnostic()? - .own_credential() - .into_diagnostic()?; - let addr = match cred_retr { - ockam_api::config::cli::CredentialRetrieverConfig::FromCredentialIssuer(c) => { - &c.multiaddr - } - _ => { - return Err(miette!( - "Trust context must be configured with a credential issuer" - )); - } + { + authority + } else { + return Err(miette!( + "Trust context must be configured with a credential issuer" + )); }; - let identity = get_identity_name(&opts.state, &cmd.cloud_opts.identity); - let authority_identifier = tc - .authority() - .into_diagnostic()? - .identifier() - .await - .into_diagnostic()?; - node.create_authority_client(&authority_identifier, addr, Some(identity)) - .await? - } else if let (Some(p), Some(a)) = get_project(&opts.state, &cmd.to).await? { - let identity = get_identity_name(&opts.state, &cmd.cloud_opts.identity); - project = Some(p); - node.create_authority_client(a.identity_id(), a.address(), Some(identity)) + let identity = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; + + node.create_authority_client(&authority.identifier(), &authority.route(), Some(identity)) .await? + } else if let Some(p) = get_project(&opts.state, &cmd.to).await? { + let identity = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; + project = Some(p.clone()); + node.create_authority_client( + &p.authority_identifier().await.into_diagnostic()?, + &p.authority_access_route().into_diagnostic()?, + Some(identity), + ) + .await? } else { return Err(miette!("Cannot create a ticket. Please specify a route to your project or to an authority node")); }; @@ -141,7 +146,7 @@ async fn run_impl( .create_token(&ctx, cmd.attributes()?, cmd.expires_in, cmd.usage_count) .await?; - let ticket = EnrollmentTicket::new(token, project, trust_context); + let ticket = EnrollmentTicket::new(token, project); let ticket_serialized = ticket.hex_encoded().into_diagnostic()?; opts.terminal .clone() @@ -156,28 +161,27 @@ async fn run_impl( /// Get the project authority from the first address protocol. /// /// If the first protocol is a `/project`, look up the project's config. -async fn get_project( - cli_state: &CliState, - input: &MultiAddr, -) -> Result<(Option, Option)> { +async fn get_project(cli_state: &CliState, input: &MultiAddr) -> Result> { if let Some(proto) = input.first() { if proto.code() == proto::Project::CODE { - let proj = proto.cast::().expect("project protocol"); - return if let Ok(p) = cli_state.projects.get(proj.to_string()) { - let c = p.config(); - let a = - ProjectAuthority::from_raw(&c.authority_access_route, &c.authority_identity) - .await?; - if a.is_some() { - let p = ProjectLookup::from_project(c).await?; - Ok((Some(p), a)) - } else { - Err(miette!("missing authority in project {:?}", &*proj).into()) + let project_name = proto.cast::().expect("project protocol"); + match cli_state.get_project_by_name(&project_name).await.ok() { + None => Err(miette!("unknown project {}", project_name.to_string()).into()), + Some(project) => { + if project.authority_identifier().await.is_err() { + Err( + miette!("missing authority in project {}", project_name.to_string()) + .into(), + ) + } else { + Ok(Some(project)) + } } - } else { - Err(miette!("unknown project {}", &*proj).into()) - }; + } + } else { + Ok(None) } + } else { + Ok(None) } - Ok((None, None)) } diff --git a/implementations/rust/ockam/ockam_command/src/project/util.rs b/implementations/rust/ockam/ockam_command/src/project/util.rs index dce94373a92..c0b337db0ca 100644 --- a/implementations/rust/ockam/ockam_command/src/project/util.rs +++ b/implementations/rust/ockam/ockam_command/src/project/util.rs @@ -1,14 +1,13 @@ use indicatif::ProgressBar; +use miette::miette; use miette::Context as _; -use miette::{miette, IntoDiagnostic}; use std::iter::Take; use std::time::Duration; use tokio_retry::strategy::FixedInterval; use tokio_retry::Retry; use tracing::debug; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; -use ockam_api::cloud::project::{Project, Projects}; +use ockam_api::cloud::project::Project; use ockam_api::cloud::{Controller, ORCHESTRATOR_AWAIT_TIMEOUT}; use ockam_api::config::lookup::LookupMeta; use ockam_api::error::ApiError; @@ -16,7 +15,6 @@ use ockam_api::nodes::service::relay::SecureChannelsCreation; use ockam_api::nodes::InMemoryNode; use ockam_api::route_to_multiaddr; -use ockam_core::compat::str::FromStr; use ockam_core::route; use ockam_multiaddr::{MultiAddr, Protocol}; use ockam_node::Context; @@ -64,19 +62,13 @@ pub async fn get_projects_secure_channels_from_config_lookup( // Get the project node's access route + identity id from the config let (project_access_route, project_identity_id) = { // This shouldn't fail, as we did a refresh above if we found any missing project. - let p = opts + let project = opts .state - .projects - .get(name) - .context(format!("Failed to get project {name} from config lookup"))? - .config() - .clone(); - let id = p - .identity - .ok_or(miette!("Project should have identity set"))?; - let node_route = MultiAddr::from_str(&p.access_route) - .into_diagnostic() - .wrap_err("Invalid project node route")?; + .get_project_by_name(name) + .await + .context(format!("Failed to get project {name}"))?; + let id = project.identifier()?; + let node_route = project.access_route()?; (node_route, id) }; @@ -113,11 +105,6 @@ pub async fn check_project_readiness( let retry_strategy = FixedInterval::from_millis(5000) .take((ORCHESTRATOR_AWAIT_TIMEOUT.as_millis() / 5000) as usize); - // Persist project config prior to checking readiness which might take a while - opts.state - .projects - .overwrite(&project.name, project.clone())?; - let spinner_option = opts.terminal.progress_spinner(); let project = check_project_ready( ctx, @@ -142,11 +129,6 @@ pub async fn check_project_readiness( if let Some(spinner) = spinner_option.as_ref() { spinner.finish_and_clear(); } - - // Persist project config with all its fields - opts.state - .projects - .overwrite(&project.name, project.clone())?; Ok(project) } @@ -166,11 +148,11 @@ async fn check_project_ready( return Ok(project); }; - let project_id = project.id.clone(); + let project_id = project.id(); let project: Project = Retry::spawn(retry_strategy.clone(), || async { // Handle the project show request result // so we can provide better errors in the case orchestrator does not respond timely - let project = controller.get_project(ctx, project_id.clone()).await?; + let project = controller.get_project(ctx, &project_id).await?; let result: miette::Result = if project.is_ready() { Ok(project) } else { @@ -238,13 +220,12 @@ async fn check_authority_node_accessible( retry_strategy: Take, spinner_option: Option, ) -> Result { - let authority = project - .authority() - .await? - .ok_or(miette!("Project does not have an authority defined."))?; - let authority_node = node - .create_authority_client(authority.identity_id(), authority.address(), None) + .create_authority_client( + &project.authority_identifier().await?, + &project.authority_access_route()?, + None, + ) .await?; if let Some(spinner) = spinner_option.as_ref() { @@ -252,25 +233,11 @@ async fn check_authority_node_accessible( } Retry::spawn(retry_strategy.clone(), || async { if authority_node.check_secure_channel(ctx).await.is_ok() { - Ok(()) - } else { - Err(miette!("Timed out while trying to establish a secure channel to the project authority. Please try again.")) - } - }) - .await?; + Ok(()) + } else { + Err(miette!("Timed out while trying to establish a secure channel to the project authority. Please try again.")) + } + }) + .await?; Ok(project) } - -pub async fn refresh_projects( - opts: &CommandGlobalOpts, - ctx: &Context, - controller: &Controller, -) -> miette::Result<()> { - let projects = controller.list_projects(ctx).await?; - for project in projects { - opts.state - .projects - .overwrite(&project.name, project.clone())?; - } - Ok(()) -} diff --git a/implementations/rust/ockam/ockam_command/src/project/version.rs b/implementations/rust/ockam/ockam_command/src/project/version.rs index caf992ff731..3c297765135 100644 --- a/implementations/rust/ockam/ockam_command/src/project/version.rs +++ b/implementations/rust/ockam/ockam_command/src/project/version.rs @@ -3,7 +3,6 @@ use colorful::Colorful; use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cloud::project::Projects; use ockam_api::nodes::InMemoryNode; use crate::util::api::CloudOpts; @@ -16,8 +15,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/version/after_long_help.txt /// Return the version of the Orchestrator Controller and the Projects #[derive(Clone, Debug, Args)] #[command( - long_about=docs::about(LONG_ABOUT), - after_long_help=docs::about(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::about(AFTER_LONG_HELP) )] pub struct VersionCommand { #[command(flatten)] @@ -38,7 +37,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts) -> miette::Result<()> // Send request let node = InMemoryNode::start(ctx, &opts.state).await?; let controller = node.create_controller().await?; - let project_version = controller.get_project_version(ctx).await?; + let project_version = controller.get_orchestrator_version_info(ctx).await?; let json = serde_json::to_string(&project_version).into_diagnostic()?; let project_version = project_version diff --git a/implementations/rust/ockam/ockam_command/src/relay/create.rs b/implementations/rust/ockam/ockam_command/src/relay/create.rs index ee7c98e7937..d3836bba553 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/create.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/create.rs @@ -18,12 +18,11 @@ use ockam_api::nodes::BackgroundNode; use ockam_multiaddr::proto::Project; use ockam_multiaddr::{MultiAddr, Protocol}; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::output::Output; use crate::terminal::OckamColor; use crate::util::{node_rpc, process_nodes_multiaddr}; -use crate::{display_parse_logs, docs, fmt_ok, CommandGlobalOpts}; -use crate::{fmt_log, Result}; +use crate::{display_parse_logs, fmt_ok, CommandGlobalOpts}; +use crate::{docs, fmt_log, Result}; const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -41,7 +40,7 @@ pub struct CreateCommand { relay_name: String, /// Node for which to create the relay - #[arg(long, id = "NODE", display_order = 900)] + #[arg(long, id = "NODE", display_order = 900, value_parser = extract_address_value)] to: Option, /// Route to the node at which to create the relay @@ -55,7 +54,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.to); node_rpc(rpc, (opts, self)); } } @@ -80,11 +78,9 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m display_parse_logs(&opts); - let to = get_node_name(&opts.state, &cmd.to); - let node_name = extract_address_value(&to)?; let at_rust_node = is_local_node(&cmd.at).wrap_err("Argument --at is not valid")?; - let ma = process_nodes_multiaddr(&cmd.at, &opts.state)?; + let ma = process_nodes_multiaddr(&cmd.at, &opts.state).await?; let alias = if at_rust_node { format!("forward_to_{}", cmd.relay_name) } else { @@ -93,13 +89,13 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m let is_finished: Mutex = Mutex::new(false); + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.to).await?; let get_relay_info = async { let relay_info = { - if cmd.at.matches(0, &[Project::CODE.into()]) && cmd.authorized.is_some() { + if cmd.at.starts_with(Project::CODE) && cmd.authorized.is_some() { return Err(miette!("--authorized can not be used with project addresses").into()); }; - info!("creating a relay at {} to {node_name}", cmd.at); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + info!("creating a relay at {} to {}", cmd.at, node.node_name()); node.create_relay(&ctx, &ma, Some(alias.clone()), cmd.authorized) .await? }; @@ -116,9 +112,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m ), format!( "Setting up receiving relay mailbox on node {}...", - &node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + &node.node_name().color(OckamColor::PrimaryResource.color()) ), ]; let progress_output = opts @@ -138,7 +132,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m .color(OckamColor::PrimaryResource.color()); let formatted_to = format!( "/node/{}{}", - &node_name, + &node.node_name(), &relay.remote_address_ma().into_diagnostic()?.to_string() ) .color(OckamColor::PrimaryResource.color()); diff --git a/implementations/rust/ockam/ockam_command/src/relay/delete.rs b/implementations/rust/ockam/ockam_command/src/relay/delete.rs index 74766d8f1c8..c3643032fee 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/delete.rs @@ -4,14 +4,14 @@ use console::Term; use miette::miette; use ockam::Context; +use ockam_api::address::extract_address_value; use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::get_node_name; use crate::relay::util::relay_name_parser; use crate::terminal::tui::DeleteCommandTui; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_ok, fmt_warn, CommandGlobalOpts, Terminal, TerminalStream}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -25,7 +25,7 @@ pub struct DeleteCommand { relay_name: Option, /// Node on which to delete the Relay. If not provided, the default node will be used - #[arg(global = true, long, value_name = "NODE")] + #[arg(global = true, long, value_name = "NODE", value_parser = extract_address_value)] pub at: Option, /// Confirm the deletion without prompting @@ -59,11 +59,7 @@ impl DeleteTui { opts: CommandGlobalOpts, cmd: DeleteCommand, ) -> miette::Result<()> { - let node_name = { - let name = get_node_name(&opts.state, &cmd.at); - parse_node_name(&name)? - }; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let tui = Self { ctx, opts, @@ -147,13 +143,13 @@ impl DeleteCommandTui for DeleteTui { plain.push_str(&fmt_ok!( "Relay with name {} on Node {} has been deleted\n", item_name.light_magenta(), - node_name.light_magenta() + node_name.clone().light_magenta() )); } else { plain.push_str(&fmt_warn!( "Failed to delete relay with name {} on Node {}\n", item_name.light_magenta(), - node_name.light_magenta() + node_name.clone().light_magenta() )); } } diff --git a/implementations/rust/ockam/ockam_command/src/relay/list.rs b/implementations/rust/ockam/ockam_command/src/relay/list.rs index 82d019e4925..d94aed8a894 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/list.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/list.rs @@ -1,18 +1,16 @@ use clap::Args; use colorful::Colorful; -use miette::{miette, IntoDiagnostic}; +use miette::IntoDiagnostic; use tokio::sync::Mutex; use tokio::try_join; use tracing::trace; use ockam::Context; use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::terminal::OckamColor; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -31,13 +29,12 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); )] pub struct ListCommand { /// Get the list of Relays at the given node - #[arg(global = true, long, value_name = "NODE")] + #[arg(global = true, long, value_name = "NODE", value_parser = extract_address_value)] pub to: Option, } impl ListCommand { pub fn run(self, options: CommandGlobalOpts) { - initialize_node_if_default(&options, &self.to); node_rpc(run_impl, (options, self)); } } @@ -46,14 +43,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let to = get_node_name(&opts.state, &cmd.to); - let node_name = extract_address_value(&to)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.to).await?; let is_finished: Mutex = Mutex::new(false); let get_relays = async { @@ -64,9 +54,7 @@ async fn run_impl( let output_messages = vec![format!( "Listing Relays on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -78,8 +66,8 @@ async fn run_impl( let plain = opts.terminal.build_list( &relays, - &format!("Relays on Node {node_name}"), - &format!("No Relays found on node {node_name}."), + &format!("Relays on Node {}", node.node_name()), + &format!("No Relays found on node {}.", node.node_name()), )?; let json = serde_json::to_string_pretty(&relays).into_diagnostic()?; diff --git a/implementations/rust/ockam/ockam_command/src/relay/show.rs b/implementations/rust/ockam/ockam_command/src/relay/show.rs index 021435d9626..fb8cfcff3b5 100644 --- a/implementations/rust/ockam/ockam_command/src/relay/show.rs +++ b/implementations/rust/ockam/ockam_command/src/relay/show.rs @@ -4,6 +4,7 @@ use indoc::formatdoc; use miette::{miette, IntoDiagnostic}; use ockam::Context; +use ockam_api::address::extract_address_value; use ockam_api::nodes::models::relay::RelayInfo; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; @@ -11,11 +12,10 @@ use ockam_multiaddr::MultiAddr; use serde::Serialize; -use crate::node::get_node_name; use crate::output::Output; use crate::relay::util::relay_name_parser; use crate::terminal::tui::ShowCommandTui; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts, Terminal, TerminalStream}; const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -33,7 +33,7 @@ pub struct ShowCommand { relay_name: Option, /// Node which the relay belongs to - #[arg(long, value_name = "NODE")] + #[arg(long, value_name = "NODE", value_parser = extract_address_value)] pub at: Option, } @@ -63,11 +63,7 @@ impl ShowTui { opts: CommandGlobalOpts, cmd: ShowCommand, ) -> miette::Result<()> { - let node_name = { - let name = get_node_name(&opts.state, &cmd.at); - parse_node_name(&name)? - }; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let tui = Self { ctx, opts, diff --git a/implementations/rust/ockam/ockam_command/src/reset.rs b/implementations/rust/ockam/ockam_command/src/reset.rs index 8c17bffb999..e6941d33d11 100644 --- a/implementations/rust/ockam/ockam_command/src/reset.rs +++ b/implementations/rust/ockam/ockam_command/src/reset.rs @@ -1,14 +1,14 @@ -use crate::terminal::ConfirmResult; -use crate::util::node_rpc; -use crate::{fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; -use ockam_api::cloud::space::Spaces; + use ockam_api::nodes::InMemoryNode; use ockam_node::Context; +use crate::terminal::ConfirmResult; +use crate::util::node_rpc; +use crate::{fmt_ok, CommandGlobalOpts}; + /// Removes the local Ockam configuration including all Identities and Nodes #[derive(Clone, Debug, Args)] pub struct ResetCommand { @@ -32,7 +32,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ResetCommand)) -> mi } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ResetCommand) -> miette::Result<()> { - let delete_orchestrator_resources = cmd.with_orchestrator && opts.state.is_enrolled()?; + let delete_orchestrator_resources = cmd.with_orchestrator && opts.state.is_enrolled().await?; if !cmd.yes { let msg = if delete_orchestrator_resources { "This will delete the local Ockam configuration and remove your spaces from the Orchestrator. Are you sure?" @@ -57,14 +57,12 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ResetCommand) -> } let node = InMemoryNode::start(ctx, &opts.state).await?; let controller = node.create_controller().await?; - for space in opts.state.spaces.list()? { + for space in opts.state.get_spaces().await? { if let Some(ref s) = spinner { - s.set_message(format!("Deleting space '{}'...", space.name())) + s.set_message(format!("Deleting space '{}'...", space.name)) } - controller - .delete_space(ctx, space.config().id.clone()) - .await?; - let _ = opts.state.spaces.delete(space.name()); + controller.delete_space(ctx, &space.id).await?; + opts.state.delete_space(&space.id).await? } if let Some(ref s) = spinner { s.finish_and_clear(); @@ -72,7 +70,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ResetCommand) -> opts.terminal .write_line(fmt_ok!("Orchestrator spaces deleted"))?; } - CliState::delete()?; + opts.state.reset().await?; opts.terminal .stdout() .plain(fmt_ok!("Local Ockam configuration deleted")) diff --git a/implementations/rust/ockam/ockam_command/src/run/parser.rs b/implementations/rust/ockam/ockam_command/src/run/parser.rs index 8e6d9b88dff..4eb58719570 100644 --- a/implementations/rust/ockam/ockam_command/src/run/parser.rs +++ b/implementations/rust/ockam/ockam_command/src/run/parser.rs @@ -1,14 +1,16 @@ -use crate::{shutdown, CommandGlobalOpts}; +use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::fmt::Debug; + use duct::Expression; use miette::IntoDiagnostic; -use ockam_api::cli_state::StateDirTrait; -use ockam_core::compat::collections::HashMap; use once_cell::sync::Lazy; use serde::Deserialize; -use std::collections::{BTreeMap, HashSet, VecDeque}; -use std::fmt::Debug; use tracing::debug; +use ockam_core::compat::collections::HashMap; + +use crate::{shutdown, CommandGlobalOpts}; + pub struct ConfigRunner { commands_sorted: Vec, commands_index: BTreeMap, @@ -130,11 +132,16 @@ impl ConfigRunner { shutdown::wait(opts.terminal, true, true, tx, &mut rx).await?; // Send a SIGTERM to all nodes if they are still running - for node_name in spawned_nodes { - if let Ok(node) = opts.state.nodes.get(node_name) { - if node.is_running() { - let _ = node.kill_process(false); - } + for node_name in &spawned_nodes { + if opts + .state + .get_node(node_name) + .await + .ok() + .map(|n| n.is_running()) + .unwrap_or(false) + { + opts.state.stop_node(node_name, false).await?; } } } diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs index cf87a7b859b..73916d853f1 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs @@ -15,15 +15,13 @@ use ockam_api::route_to_multiaddr; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::docs; -use crate::identity::{get_identity_name, initialize_identity_if_default}; - use crate::project::util::{ clean_projects_multiaddr, get_projects_secure_channels_from_config_lookup, }; use crate::util::api::CloudOpts; use crate::util::clean_nodes_multiaddr; use crate::{ + docs, error::Error, fmt_log, fmt_ok, terminal::OckamColor, @@ -37,13 +35,13 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create Secure Channels #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct CreateCommand { /// Node from which to initiate the secure channel - #[arg(value_name = "NODE", long, display_order = 800)] + #[arg(value_name = "NODE", long, display_order = 800, value_parser = extract_address_value)] pub from: String, /// Route to a secure channel listener @@ -65,7 +63,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_identity_if_default(&opts, &self.cloud_opts.identity); node_rpc(rpc, (opts, self)); } @@ -78,10 +75,12 @@ impl CreateCommand { node: &BackgroundNode, ) -> miette::Result { let (to, meta) = clean_nodes_multiaddr(&self.to, &opts.state) - .into_diagnostic() + .await .wrap_err(format!("Could not convert {} into route", &self.to))?; - - let identity_name = get_identity_name(&opts.state, &self.cloud_opts.identity); + let identity_name = opts + .state + .get_identity_name_or_default(&self.cloud_opts.identity) + .await?; let projects_sc = get_projects_secure_channels_from_config_lookup( opts, @@ -96,27 +95,24 @@ impl CreateCommand { .into_diagnostic() .wrap_err("Could not parse projects from route") } - - // Read the `from` argument and return node name - fn parse_from_node(&self) -> miette::Result { - extract_address_value(&self.from).into_diagnostic() - } } async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> miette::Result<()> { + let node = BackgroundNode::create_to_node(&ctx, &opts.state, &cmd.from).await?; + opts.terminal .write_line(&fmt_log!("Creating Secure Channel...\n"))?; // Delegate the request to create a secure channel to the from node. let is_finished: Mutex = Mutex::new(false); - - let from = cmd.parse_from_node()?; - let node = BackgroundNode::create(&ctx, &opts.state, &from).await?; let to = cmd.parse_to_route(&opts, &ctx, &node).await?; let authorized_identifiers = cmd.authorized.clone(); let create_secure_channel = async { - let identity_name = get_identity_name(&opts.state, &cmd.cloud_opts.identity); + let identity_name = opts + .state + .get_identity_name_or_default(&cmd.cloud_opts.identity) + .await?; let payload = CreateSecureChannelRequest::new( &to, authorized_identifiers, @@ -145,7 +141,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> m ) })?; - let from = format!("/node/{}", from); + let from = format!("/node/{}", node.node_name()); opts.terminal .stdout() .plain( diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/delete.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/delete.rs index ccdedb80c4a..18f2955e0bb 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/delete.rs @@ -5,13 +5,13 @@ use colorful::Colorful; use serde_json::json; use ockam::{route, Context}; +use ockam_api::address::extract_address_value; use ockam_api::nodes::BackgroundNode; use ockam_api::{nodes::models::secure_channel::DeleteSecureChannelResponse, route_to_multiaddr}; use ockam_core::{Address, AddressParseError}; use crate::docs; -use crate::node::get_node_name; -use crate::util::{is_tty, parse_node_name}; +use crate::util::is_tty; use crate::{ util::{api, exitcode, node_rpc}, CommandGlobalOpts, OutputFormat, @@ -29,7 +29,7 @@ after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct DeleteCommand { /// Node at which the secure channel was initiated - #[arg(value_name = "NODE", long, display_order = 800)] + #[arg(value_name = "NODE", long, display_order = 800, value_parser = extract_address_value)] at: Option, /// Address at which the channel to be deleted is running @@ -152,13 +152,11 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand)) -> m cmd.yes, "Are you sure you want to delete this secure channel?", )? { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let address = &cmd.address; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; let response: DeleteSecureChannelResponse = node.ask(&ctx, api::delete_secure_channel(address)).await?; - cmd.print_output(&node_name, address, &opts, response); + cmd.print_output(&node.node_name(), address, &opts, response); } Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/list.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/list.rs index 69592589ca2..ef23e4fd5f5 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/list.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/list.rs @@ -7,16 +7,13 @@ use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::secure_channel::ShowSecureChannelResponse; use ockam_api::nodes::BackgroundNode; use ockam_api::route_to_multiaddr; use ockam_core::{route, Address}; -use crate::node::get_node_name; use crate::output::Output; use crate::terminal::OckamColor; -use crate::util::parse_node_name; use crate::{ docs, util::{api, node_rpc}, @@ -30,10 +27,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Secure Channels #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ListCommand { /// Node at which the returned secure channels were initiated @@ -84,16 +81,9 @@ impl ListCommand { } async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let is_finished: Mutex = Mutex::new(false); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; - let get_secure_channel_identifiers = async { let secure_channel_identifiers: Vec = node.ask(&ctx, api::list_secure_channels()).await?; @@ -115,7 +105,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie let request = api::show_secure_channel(&Address::from(channel_addr)); let show_response: ShowSecureChannelResponse = node.ask(&ctx, request).await?; let secure_channel_output = - cmd.build_output(&node_name, channel_addr, show_response)?; + cmd.build_output(&node.node_name(), channel_addr, show_response)?; *is_finished.lock().await = true; Ok(secure_channel_output) }; @@ -136,8 +126,8 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie let list = opts.terminal.build_list( &responses, - &format!("Secure Channels on {}", node_name), - &format!("No secure channels found on {}", node_name), + &format!("Secure Channels on {}", node.node_name()), + &format!("No secure channels found on {}", node.node_name()), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/create.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/create.rs index 8ba28311888..080470f79aa 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/create.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/create.rs @@ -9,8 +9,8 @@ use ockam_api::nodes::{BackgroundNode, NODEMANAGER_ADDR}; use ockam_core::api::{Request, Status}; use ockam_core::{Address, Route}; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{api, exitcode, node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::{api, exitcode, node_rpc}; use crate::{docs, fmt_log, fmt_ok, terminal::OckamColor, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -19,9 +19,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create Secure Channel Listeners #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct CreateCommand { #[command(flatten)] @@ -45,7 +45,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -58,9 +57,7 @@ async fn run_impl( ctx: &Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&at)?; - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = Request::post("/node/secure_channel_listener").body( CreateSecureChannelListenerRequest::new( &cmd.address, @@ -83,9 +80,7 @@ async fn run_impl( .color(OckamColor::PrimaryResource.color()) ) + &fmt_log!( "At node /node/{}", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) ), ) .machine(address.to_string()) diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/delete.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/delete.rs index f6f9c7e296d..1126249b2e4 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/delete.rs @@ -6,8 +6,8 @@ use ockam_api::nodes::models::secure_channel::DeleteSecureChannelListenerRespons use ockam_api::nodes::BackgroundNode; use ockam_core::Address; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{api, node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::{api, node_rpc}; use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/delete/long_about.txt"); @@ -16,9 +16,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt" /// Delete Secure Channel Listeners #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct DeleteCommand { /// Address at which the channel listener to be deleted is running @@ -30,7 +30,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -43,16 +42,15 @@ async fn run_impl( ctx: &Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&at)?; - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let req = api::delete_secure_channel_listener(&cmd.address); let response: DeleteSecureChannelListenerResponse = node.ask(ctx, req).await?; let addr = response.addr; opts.terminal .stdout() .plain(fmt_ok!( - "Deleted secure-channel listener with address '{addr}' on node '{node_name}'" + "Deleted secure-channel listener with address '{addr}' on node '{}'", + node.node_name() )) .machine(addr) .write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/list.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/list.rs index f394a78c74f..9d1b741521d 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/list.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/list.rs @@ -5,7 +5,6 @@ use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::secure_channel::{ SecureChannelListenersList, ShowSecureChannelListenerResponse, }; @@ -13,11 +12,11 @@ use ockam_api::nodes::BackgroundNode; use ockam_api::route_to_multiaddr; use ockam_core::route; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::output::Output; use crate::terminal::OckamColor; +use crate::util::api; use crate::util::node_rpc; -use crate::util::{api, parse_node_name}; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); @@ -27,10 +26,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List Secure Channel Listeners #[derive(Args, Clone, Debug)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ListCommand { /// Node of which secure listeners shall be listed @@ -40,7 +39,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -50,14 +48,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&at)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_listeners = async { @@ -69,9 +60,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let output_messages = vec![format!( "Listing secure channel listeners on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -82,8 +71,11 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let list = opts.terminal.build_list( &secure_channel_listeners.list, - &format!("Secure Channel Listeners at Node {}", node_name), - &format!("No secure channel listeners found at node {}.", node_name), + &format!("Secure Channel Listeners at Node {}", node.node_name()), + &format!( + "No secure channel listeners found at node {}.", + node.node_name() + ), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/show.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/show.rs index 702cc05ffc1..6b373e2fa70 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/listener/show.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/listener/show.rs @@ -4,8 +4,8 @@ use ockam::Context; use ockam_api::nodes::BackgroundNode; use ockam_core::Address; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; -use crate::util::{api, node_rpc, parse_node_name}; +use crate::node::NodeOpts; +use crate::util::{api, node_rpc}; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/show/long_about.txt"); @@ -15,10 +15,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show Secure Channel Listener #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ShowCommand { /// Address of the channel listener @@ -30,7 +30,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -43,11 +42,8 @@ async fn run_impl( ctx: &Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&at)?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let address = &cmd.address; - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; let req = api::show_secure_channel_listener(address); node.tell(ctx, req).await?; opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/show.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/show.rs index 3a1da7ec893..2f0e7d10fdf 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/show.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/show.rs @@ -6,9 +6,7 @@ use ockam_api::nodes::models::secure_channel::ShowSecureChannelResponse; use ockam_api::nodes::BackgroundNode; use ockam_core::Address; -use crate::node::get_node_name; use crate::output::Output; -use crate::util::parse_node_name; use crate::{ docs, util::{api, node_rpc}, @@ -22,10 +20,10 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show Secure Channels #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP), +arg_required_else_help = true, +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP), )] pub struct ShowCommand { /// Node at which the secure channel was initiated @@ -44,11 +42,9 @@ impl ShowCommand { } async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand)) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&at)?; - let address = &cmd.address; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let address = &cmd.address; let response: ShowSecureChannelResponse = node.ask(&ctx, api::show_secure_channel(address)).await?; opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/service/config.rs b/implementations/rust/ockam/ockam_command/src/service/config.rs index fb7fb73ed83..629dda96ab7 100644 --- a/implementations/rust/ockam/ockam_command/src/service/config.rs +++ b/implementations/rust/ockam/ockam_command/src/service/config.rs @@ -4,7 +4,7 @@ use miette::{Context as _, IntoDiagnostic}; use serde::{Deserialize, Serialize}; use ockam::identity::Identifier; -use ockam_api::DefaultAddress; +use ockam_api::nodes::service::default_address::DefaultAddress; use crate::Result; diff --git a/implementations/rust/ockam/ockam_command/src/service/list.rs b/implementations/rust/ockam/ockam_command/src/service/list.rs index 59afdf5a9ed..c604c525f71 100644 --- a/implementations/rust/ockam/ockam_command/src/service/list.rs +++ b/implementations/rust/ockam/ockam_command/src/service/list.rs @@ -2,20 +2,18 @@ use std::fmt::Write; use clap::Args; use colorful::Colorful; -use miette::miette; use miette::IntoDiagnostic; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::services::{ServiceList, ServiceStatus}; use ockam_api::nodes::BackgroundNode; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::output::Output; use crate::terminal::OckamColor; -use crate::util::{api, node_rpc, parse_node_name}; +use crate::util::{api, node_rpc}; use crate::CommandGlobalOpts; /// List service(s) of a given node @@ -27,7 +25,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -37,14 +34,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_services = async { @@ -55,9 +45,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let output_messages = vec![format!( "Listing Services on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -68,8 +56,8 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let plain = opts.terminal.build_list( &services.list, - &format!("Services on {}", node_name), - &format!("No services found on {}", node_name), + &format!("Services on {}", node.node_name()), + &format!("No services found on {}", node.node_name()), )?; let json = serde_json::to_string_pretty(&services.list).into_diagnostic()?; opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/service/start.rs b/implementations/rust/ockam/ockam_command/src/service/start.rs index bb91d2ddd38..179e76a4969 100644 --- a/implementations/rust/ockam/ockam_command/src/service/start.rs +++ b/implementations/rust/ockam/ockam_command/src/service/start.rs @@ -4,11 +4,11 @@ use miette::miette; use minicbor::Encode; use ockam::Context; +use ockam_api::nodes::service::default_address::DefaultAddress; use ockam_api::nodes::BackgroundNode; -use ockam_api::DefaultAddress; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::terminal::OckamColor; use crate::util::{api, node_rpc}; use crate::{fmt_ok, CommandGlobalOpts}; @@ -70,7 +70,6 @@ fn authenticator_default_addr() -> String { impl StartCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -80,8 +79,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, StartCommand)) -> mi } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: StartCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let mut is_hop_service = false; let addr = match cmd.create_subcommand { StartSubCommand::Hop { addr, .. } => { diff --git a/implementations/rust/ockam/ockam_command/src/space/create.rs b/implementations/rust/ockam/ockam_command/src/space/create.rs index 9af19eadb1a..f3e377feacf 100644 --- a/implementations/rust/ockam/ockam_command/src/space/create.rs +++ b/implementations/rust/ockam/ockam_command/src/space/create.rs @@ -1,14 +1,14 @@ use clap::Args; -use ockam::Context; -use ockam_api::cloud::space::Spaces; +use colorful::Colorful; +use miette::miette; use crate::output::Output; use crate::util::api::{self, CloudOpts}; -use crate::util::{is_enrolled_guard, node_rpc}; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; -use colorful::Colorful; +use ockam::Context; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::{SpaceConfig, StateDirTrait}; +use ockam_api::cloud::space::Spaces; use ockam_api::nodes::InMemoryNode; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); @@ -17,8 +17,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create a new space #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct CreateCommand { /// Name of the space - must be unique across all Ockam Orchestrator users. @@ -48,7 +48,15 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - is_enrolled_guard(&opts.state, None)?; + if !opts + .state + .is_identity_enrolled(&cmd.cloud_opts.identity) + .await? + { + return Err(miette!( + "Please enroll using 'ockam enroll' before using this command" + )); + }; opts.terminal.write_line(format!( "\n{}", @@ -61,24 +69,26 @@ async fn run_impl( ))?; let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; - let space = controller.create_space(ctx, cmd.name, cmd.admins).await?; + let space = node + .create_space( + ctx, + &cmd.name, + cmd.admins.iter().map(|a| a.as_ref()).collect(), + ) + .await?; opts.terminal .stdout() .plain(space.output()?) .json(serde_json::json!(&space)) .write_line()?; - opts.state - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; Ok(()) } fn validate_space_name(s: &str) -> Result { match api::validate_cloud_resource_name(s) { Ok(_) => Ok(s.to_string()), - Err(_e)=> Err(String::from( + Err(_e) => Err(String::from( "space name can contain only alphanumeric characters and the '-', '_' and '.' separators. \ Separators must occur between alphanumeric characters. This implies that separators can't \ occur at the start or end of the name, nor they can occur in sequence.", diff --git a/implementations/rust/ockam/ockam_command/src/space/delete.rs b/implementations/rust/ockam/ockam_command/src/space/delete.rs index a22987fe154..343f8a1ff32 100644 --- a/implementations/rust/ockam/ockam_command/src/space/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/space/delete.rs @@ -3,9 +3,7 @@ use colorful::Colorful; use console::Term; use ockam::Context; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::cloud::space::Spaces; -use ockam_api::cloud::Controller; use ockam_api::nodes::InMemoryNode; use crate::terminal::tui::DeleteCommandTui; @@ -51,7 +49,7 @@ async fn run_impl( pub struct DeleteTui { ctx: Context, opts: CommandGlobalOpts, - controller: Controller, + node: InMemoryNode, cmd: DeleteCommand, } @@ -62,11 +60,10 @@ impl DeleteTui { cmd: DeleteCommand, ) -> miette::Result<()> { let node = InMemoryNode::start(&ctx, &opts.state).await?; - let controller = node.create_controller().await?; let tui = Self { ctx, opts, - controller, + node, cmd, }; tui.delete().await @@ -95,20 +92,25 @@ impl DeleteCommandTui for DeleteTui { async fn get_arg_item_name_or_default(&self) -> miette::Result { let space_name = match &self.cmd.space_name { - None => self.opts.state.spaces.default()?.name().to_string(), + None => self.opts.state.get_default_space().await?.space_name(), Some(n) => n.to_string(), }; Ok(space_name) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.spaces.list_items_names()?) + Ok(self + .opts + .state + .get_spaces() + .await? + .iter() + .map(|s| s.space_name()) + .collect()) } async fn delete_single(&self, item_name: &str) -> miette::Result<()> { - let space_id = self.opts.state.spaces.get(item_name)?.config().id.clone(); - self.controller.delete_space(&self.ctx, space_id).await?; - let _ = self.opts.state.spaces.delete(item_name); + self.node.delete_space_by_name(&self.ctx, item_name).await?; self.terminal() .stdout() diff --git a/implementations/rust/ockam/ockam_command/src/space/list.rs b/implementations/rust/ockam/ockam_command/src/space/list.rs index 12918ba8e61..9f1b4890983 100644 --- a/implementations/rust/ockam/ockam_command/src/space/list.rs +++ b/implementations/rust/ockam/ockam_command/src/space/list.rs @@ -4,7 +4,6 @@ use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::{SpaceConfig, StateDirTrait}; use ockam_api::cloud::space::Spaces; use ockam_api::nodes::InMemoryNode; @@ -42,10 +41,9 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, _cmd: ListCommand) -> miette::Result<()> { let is_finished: Mutex = Mutex::new(false); let node = InMemoryNode::start(ctx, &opts.state).await?; - let controller = node.create_controller().await?; let get_spaces = async { - let spaces = controller.list_spaces(ctx).await?; + let spaces = node.get_spaces(ctx).await?; *is_finished.lock().await = true; Ok(spaces) }; @@ -65,12 +63,6 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, _cmd: ListCommand) -> )?; let json = serde_json::to_string_pretty(&spaces).into_diagnostic()?; - for space in spaces { - opts.state - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; - } - opts.terminal .stdout() .plain(plain) diff --git a/implementations/rust/ockam/ockam_command/src/space/mod.rs b/implementations/rust/ockam/ockam_command/src/space/mod.rs index c72cac8a515..1e05a5d97bf 100644 --- a/implementations/rust/ockam/ockam_command/src/space/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/space/mod.rs @@ -11,7 +11,6 @@ mod create; mod delete; mod list; mod show; -mod util; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); diff --git a/implementations/rust/ockam/ockam_command/src/space/show.rs b/implementations/rust/ockam/ockam_command/src/space/show.rs index ae03764f90a..df959c15107 100644 --- a/implementations/rust/ockam/ockam_command/src/space/show.rs +++ b/implementations/rust/ockam/ockam_command/src/space/show.rs @@ -3,9 +3,7 @@ use console::Term; use miette::IntoDiagnostic; use ockam::Context; -use ockam_api::cli_state::{SpaceConfig, StateDirTrait, StateItemTrait}; use ockam_api::cloud::space::{Space, Spaces}; -use ockam_api::cloud::Controller; use ockam_api::nodes::InMemoryNode; use crate::output::Output; @@ -21,9 +19,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show the details of a space #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ShowCommand { /// Name of the space @@ -51,7 +49,7 @@ pub struct ShowTui { ctx: Context, opts: CommandGlobalOpts, space_name: Option, - controller: Controller, + node: InMemoryNode, } impl ShowTui { @@ -61,12 +59,11 @@ impl ShowTui { cmd: ShowCommand, ) -> miette::Result<()> { let node = InMemoryNode::start(&ctx, &opts.state).await?; - let controller = node.create_controller().await?; let tui = Self { ctx, opts, space_name: cmd.name, - controller, + node, }; tui.show().await } @@ -86,23 +83,24 @@ impl ShowCommandTui for ShowTui { async fn get_arg_item_name_or_default(&self) -> miette::Result { let space_name = match &self.space_name { - None => self.opts.state.spaces.default()?.name().to_string(), + None => self.opts.state.get_default_space().await?.space_name(), Some(n) => n.to_string(), }; Ok(space_name) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.spaces.list_items_names()?) + Ok(self + .node + .get_spaces(&self.ctx) + .await? + .iter() + .map(|s| s.space_name()) + .collect()) } async fn show_single(&self, item_name: &str) -> miette::Result<()> { - let id = self.opts.state.spaces.get(item_name)?.config().id.clone(); - let space = self.controller.get_space(&self.ctx, id).await?; - self.opts - .state - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; + let space = self.node.get_space_by_name(&self.ctx, item_name).await?; self.terminal() .stdout() .plain(space.output()?) @@ -113,13 +111,7 @@ impl ShowCommandTui for ShowTui { } async fn show_multiple(&self, items_names: Vec) -> miette::Result<()> { - let spaces: Vec = self.controller.list_spaces(&self.ctx).await?; - for space in &spaces { - self.opts - .state - .spaces - .overwrite(&space.name, SpaceConfig::from(space))?; - } + let spaces: Vec = self.node.get_spaces(&self.ctx).await?; let filtered: Vec = spaces .into_iter() .filter(|s| items_names.contains(&s.name)) diff --git a/implementations/rust/ockam/ockam_command/src/space/util.rs b/implementations/rust/ockam/ockam_command/src/space/util.rs deleted file mode 100644 index 24a09e4f42e..00000000000 --- a/implementations/rust/ockam/ockam_command/src/space/util.rs +++ /dev/null @@ -1,21 +0,0 @@ -use ockam::Context; -use ockam_api::cli_state::{SpaceConfig, StateDirTrait}; -use ockam_api::cloud::space::Spaces; -use ockam_api::cloud::Controller; - -use crate::CommandGlobalOpts; - -#[allow(dead_code)] -async fn refresh_spaces( - ctx: &Context, - opts: &CommandGlobalOpts, - controller: &Controller, -) -> miette::Result<()> { - let spaces = controller.list_spaces(ctx).await?; - for space in spaces { - opts.state - .spaces - .overwrite(&space.name, SpaceConfig::from(&space))?; - } - Ok(()) -} diff --git a/implementations/rust/ockam/ockam_command/src/status.rs b/implementations/rust/ockam/ockam_command/src/status.rs index df33ed42a8a..1e18c124103 100644 --- a/implementations/rust/ockam/ockam_command/src/status.rs +++ b/implementations/rust/ockam/ockam_command/src/status.rs @@ -2,20 +2,14 @@ use std::io::Write; use std::time::Duration; use clap::Args; -use miette::miette; -use minicbor::{Decode, Decoder, Encode}; use tracing::warn; -use ockam::identity::{Identifier, SecureChannelOptions, TrustIdentifierPolicy}; -use ockam::{Context, Node, TcpConnectionOptions, TcpTransport}; -use ockam_api::cli_state::identities::IdentityState; -use ockam_api::cli_state::traits::{StateDirTrait, StateItemTrait}; -use ockam_api::cli_state::NodeState; +use ockam::identity::{Identifier, TimestampInSeconds}; +use ockam::Context; +use ockam_api::cli_state::{EnrollmentStatus, IdentityEnrollment}; +use ockam_api::cloud::project::OrchestratorVersionInfo; use ockam_api::nodes::models::base::NodeStatus as NodeStatusModel; -use ockam_api::nodes::{BackgroundNode, NodeManager}; -use ockam_core::api::{Request, ResponseHeader, Status}; -use ockam_core::route; -use ockam_node::MessageSendReceiveOptions; +use ockam_api::nodes::{BackgroundNode, InMemoryNode}; use crate::util::{api, node_rpc}; use crate::CommandGlobalOpts; @@ -48,32 +42,40 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: StatusCommand, ) -> miette::Result<()> { - let identities_details = get_identities_details(&opts, cmd.all)?; + let identities_details = get_identities_details(&opts, cmd.all).await?; let nodes_details = get_nodes_details(ctx, &opts).await?; - let orchestrator_version = - get_orchestrator_version(ctx, &opts, Duration::from_secs(cmd.timeout)).await; + + let node = InMemoryNode::start(ctx, &opts.state).await?; + let controller = node.create_controller().await?; + + let orchestrator_version = controller + .get_orchestrator_version_info(ctx) + .await + .map_err(|e| warn!(%e, "Failed to retrieve orchestrator version")) + .unwrap_or_default(); let status = StatusData::from_parts(orchestrator_version, identities_details, nodes_details)?; - print_output(opts, cmd, status)?; + print_output(opts, cmd, status).await?; Ok(()) } async fn get_nodes_details(ctx: &Context, opts: &CommandGlobalOpts) -> Result> { let mut node_details: Vec = vec![]; - let node_states = opts.state.nodes.list()?; - if node_states.is_empty() { + let nodes = opts.state.get_nodes().await?; + if nodes.is_empty() { return Ok(node_details); } - let default_node_name = opts.state.nodes.default()?.name().to_string(); - let mut node = BackgroundNode::create(ctx, &opts.state, &default_node_name).await?; - node.set_timeout(Duration::from_millis(200)); + let default_node_name = opts.state.get_default_node().await?.name(); + let mut node_client = + BackgroundNode::create_to_node(ctx, &opts.state, &default_node_name).await?; + node_client.set_timeout(Duration::from_millis(200)); - for node_state in &node_states { - node.set_node_name(node_state.name()); + for node in nodes { + node_client.set_node_name(&node.name()); let node_infos = NodeDetails { - identifier: node_state.config().identifier()?, - state: node_state.clone(), - status: get_node_status(ctx, &node).await?, + identifier: node.identifier(), + name: node.name(), + status: get_node_status(ctx, &node_client).await?, }; node_details.push(node_infos); } @@ -89,94 +91,27 @@ async fn get_node_status(ctx: &Context, node: &BackgroundNode) -> Result .unwrap_or("Stopped".to_string())) } -fn get_identities_details(opts: &CommandGlobalOpts, all: bool) -> Result> { - let mut identities_details: Vec = vec![]; - for identity in opts.state.identities.list()? { - if all { - identities_details.push(identity) - } else { - match &identity.config().enrollment_status { - Some(_enrollment) => identities_details.push(identity), - None => (), - } - } - } - Ok(identities_details) -} - -async fn get_orchestrator_version( - ctx: &Context, +async fn get_identities_details( opts: &CommandGlobalOpts, - timeout: Duration, -) -> Result { - // for new we get the controller address directly until we - // access a Controller interface from the NodeManager - let controller_addr = NodeManager::controller_multiaddr(); - let controller_identifier = NodeManager::load_controller_identifier()?; - let controller_tcp_addr = controller_addr.to_socket_addr()?; - let tcp = TcpTransport::create(ctx).await?; - let connection = tcp - .connect(controller_tcp_addr, TcpConnectionOptions::new()) - .await?; - - // Create node that will be used to send the request - let node = { - // Get or create a vault to store the identity - let vault = match opts.state.vaults.default() { - Ok(v) => v, - Err(_) => opts.state.create_vault_state(None).await?, - } - .get() - .await?; - let identities_repository = opts.state.identities.identities_repository().await?; - Node::builder() - .with_vault(vault) - .with_identities_repository(identities_repository) - .build(ctx) - .await? + all: bool, +) -> Result> { + let enrollment_status = if all { + EnrollmentStatus::Any + } else { + EnrollmentStatus::Enrolled }; - - // Establish secure channel with controller - let node_identifier = opts + Ok(opts .state - .default_identities() - .await? - .identities_creation() - .create_identity() - .await?; - let secure_channel_options = SecureChannelOptions::new() - .with_trust_policy(TrustIdentifierPolicy::new(controller_identifier)) - .with_timeout(timeout); - let secure_channel = node - .create_secure_channel( - &node_identifier, - route![connection, "api"], - secure_channel_options, - ) - .await?; - - // Send request - let buf: Vec = node - .send_and_receive_extended::>( - route![secure_channel, "version_info"], - Request::get("").to_vec()?, - MessageSendReceiveOptions::new().with_timeout(timeout), - ) - .await? - .body(); - let mut dec = Decoder::new(&buf); - - // Decode response - let hdr = dec.decode::()?; - if hdr.status() == Some(Status::Ok) { - Ok(dec.decode::()?) - } else { - Err(miette!("Failed to retrieve version information from node.").into()) - } + .get_identity_enrollments(enrollment_status) + .await?) } -fn print_output(opts: CommandGlobalOpts, cmd: StatusCommand, status: StatusData) -> Result<()> { - let plain = build_plain_output(&opts, &cmd, &status)?; +async fn print_output( + opts: CommandGlobalOpts, + cmd: StatusCommand, + status: StatusData, +) -> Result<()> { + let plain = build_plain_output(&cmd, &status).await?; let json = serde_json::to_string(&status)?; opts.terminal .stdout() @@ -186,21 +121,17 @@ fn print_output(opts: CommandGlobalOpts, cmd: StatusCommand, status: StatusData) Ok(()) } -fn build_plain_output( - opts: &CommandGlobalOpts, - cmd: &StatusCommand, - status: &StatusData, -) -> Result> { +async fn build_plain_output(cmd: &StatusCommand, status: &StatusData) -> Result> { let mut plain = Vec::new(); writeln!( &mut plain, "Controller version: {}", - status.orchestrator_version.controller_version + status.orchestrator_version.version() )?; writeln!( &mut plain, "Project version: {}", - status.orchestrator_version.project_version + status.orchestrator_version.project_version() )?; if status.identities.is_empty() { if cmd.all { @@ -214,16 +145,19 @@ fn build_plain_output( )?; } return Ok(plain); - } - let default_identity = opts.state.identities.default()?; + }; + for (i_idx, i) in status.identities.iter().enumerate() { writeln!(&mut plain, "Identity[{i_idx}]")?; - if default_identity.config().identifier() == i.identity.config().identifier() { + if i.is_default() { writeln!(&mut plain, "{:2}Default: yes", "")?; } - for line in i.identity.to_string().lines() { - writeln!(&mut plain, "{:2}{}", "", line)?; + if let Some(name) = i.name() { + writeln!(&mut plain, "{:2}{}", "Name", name)?; } + writeln!(&mut plain, "{:2}{}", "Identifier", i.identifier())?; + writeln!(&mut plain, "{:2}{}", "Enrolled", i.is_enrolled())?; + if !i.nodes.is_empty() { writeln!(&mut plain, "{:2}Linked Nodes:", "")?; for (n_idx, node) in i.nodes.iter().enumerate() { @@ -245,31 +179,24 @@ struct StatusData { impl StatusData { fn from_parts( - orchestrator_version: Result, - identities_details: Vec, + orchestrator_version: OrchestratorVersionInfo, + identities_details: Vec, mut nodes_details: Vec, ) -> Result { - let orchestrator_version = orchestrator_version - .map_err(|e| warn!(%e, "Failed to retrieve orchestrator version")) - .unwrap_or(OrchestratorVersionInfo { - controller_version: "N/A".to_string(), - project_version: "N/A".to_string(), - }); let mut identities = vec![]; for identity in identities_details.into_iter() { let mut identity_status = IdentityWithLinkedNodes { - identity, + identifier: identity.identifier(), + name: identity.name(), + is_default: identity.is_default(), + enrolled_at: identity + .enrolled_at() + .map(|o| TimestampInSeconds::from(o.unix_timestamp() as u64)), nodes: vec![], }; - nodes_details - .retain(|nd| nd.identifier == identity_status.identity.config().identifier()); + nodes_details.retain(|nd| nd.identifier == identity_status.identifier()); if !nodes_details.is_empty() { - for node in nodes_details.iter() { - identity_status.nodes.push(NodeStatus { - name: node.state.name().to_string(), - status: node.status.clone(), - }); - } + identity_status.nodes = nodes_details.clone(); } identities.push(identity_status); } @@ -282,31 +209,39 @@ impl StatusData { #[derive(serde::Serialize, serde::Deserialize)] struct IdentityWithLinkedNodes { - identity: IdentityState, - nodes: Vec, + identifier: Identifier, + name: Option, + is_default: bool, + enrolled_at: Option, + nodes: Vec, } -#[derive(serde::Serialize, serde::Deserialize)] -struct IdentityStatus {} +impl IdentityWithLinkedNodes { + fn identifier(&self) -> Identifier { + self.identifier.clone() + } -#[derive(serde::Serialize, serde::Deserialize)] -struct NodeStatus { - name: String, - status: String, + fn name(&self) -> Option { + self.name.clone() + } + + fn is_default(&self) -> bool { + self.is_default + } + + fn is_enrolled(&self) -> bool { + self.enrolled_at.is_some() + } + + #[allow(unused)] + fn nodes(&self) -> &Vec { + &self.nodes + } } -struct NodeDetails { +#[derive(serde::Serialize, serde::Deserialize, Clone)] +pub struct NodeDetails { identifier: Identifier, - state: NodeState, + name: String, status: String, } - -#[derive(Encode, Decode, Debug, serde::Serialize, serde::Deserialize)] -#[cfg_attr(test, derive(Clone))] -#[cbor(map)] -struct OrchestratorVersionInfo { - #[n(1)] - controller_version: String, - #[n(2)] - project_version: String, -} diff --git a/implementations/rust/ockam/ockam_command/src/tcp/connection/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/connection/create.rs index f763fb41b87..83c57cd6464 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/connection/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/connection/create.rs @@ -1,13 +1,16 @@ use clap::Args; +use colorful::Colorful; use indoc::formatdoc; use miette::IntoDiagnostic; +use serde_json::json; use ockam_api::address::extract_address_value; use ockam_api::nodes::models::transport::TransportStatus; use ockam_api::nodes::BackgroundNode; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; +use crate::output::OutputFormat; +use crate::util::is_tty; use crate::{ docs, util::{api, node_rpc}, @@ -20,7 +23,7 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" #[command(after_long_help = docs::after_help(AFTER_LONG_HELP))] pub struct TcpConnectionNodeOpts { /// Node that will initiate the connection - #[arg(global = true, short, long, value_name = "NODE")] + #[arg(global = true, short, long, value_name = "NODE", value_parser = extract_address_value)] pub from: Option, } @@ -38,20 +41,69 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.from); node_rpc(run_impl, (opts, self)) } + + #[allow(unused)] + async fn print_output( + &self, + opts: &CommandGlobalOpts, + response: &TransportStatus, + ) -> miette::Result<()> { + // if output format is json, write json to stdout. + match opts.global_args.output_format { + OutputFormat::Plain => { + if !is_tty(std::io::stdout()) { + println!("{}", response.multiaddr().into_diagnostic()?); + return Ok(()); + } + let from = opts + .state + .get_node_or_default(&self.node_opts.from) + .await? + .name(); + let to = response.socket_addr().into_diagnostic()?; + if opts.global_args.no_color { + println!("\n TCP Connection:"); + println!(" From: /node/{from}"); + println!(" To: {} (/ip4/{}/tcp/{})", to, to.ip(), to.port()); + println!(" Address: {}", response.multiaddr().into_diagnostic()?); + } else { + println!("\n TCP Connection:"); + println!("{}", format!(" From: /node/{from}").light_magenta()); + println!( + "{}", + format!(" To: {} (/ip4/{}/tcp/{})", to, to.ip(), to.port()) + .light_magenta() + ); + println!( + "{}", + format!(" Address: {}", response.multiaddr().into_diagnostic()?) + .light_magenta() + ); + } + } + OutputFormat::Json => { + let json = json!([{"route": response.multiaddr().into_diagnostic()? }]); + println!("{json}"); + } + } + Ok(()) + } } async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let from = get_node_name(&opts.state, &cmd.node_opts.from); - let node_name = extract_address_value(&from)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.from).await?; let request = api::create_tcp_connection(&cmd); let transport_status: TransportStatus = node.ask(&ctx, request).await?; + let from = opts + .state + .get_node_or_default(&cmd.node_opts.from) + .await? + .name(); let to = transport_status.socket_addr().into_diagnostic()?; let plain = formatdoc! {r#" TCP Connection: @@ -59,7 +111,7 @@ async fn run_impl( To: {to} (/ip4/{}/tcp/{}) Address: {} "#, to.ip(), to.port(), transport_status.multiaddr().into_diagnostic()?}; - let json = serde_json::json!([{"route": transport_status.multiaddr().into_diagnostic()? }]); + let json = json!([{"route": transport_status.multiaddr().into_diagnostic()? }]); opts.terminal .stdout() .plain(plain) diff --git a/implementations/rust/ockam/ockam_command/src/tcp/connection/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/connection/delete.rs index f61b067c8a8..3ef5389b71e 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/connection/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/connection/delete.rs @@ -5,7 +5,6 @@ use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::util::node_rpc; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; @@ -28,7 +27,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -41,9 +39,8 @@ async fn run_impl( cmd.yes, "Are you sure you want to delete this TCP connection?", )? { + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let address = cmd.address; - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; let req = Request::delete("/node/tcp/connection") .body(models::transport::DeleteTransport::new(address.clone())); node.tell(&ctx, req).await?; diff --git a/implementations/rust/ockam/ockam_command/src/tcp/connection/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/connection/list.rs index 528dc4abfc0..19ff3714ff6 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/connection/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/connection/list.rs @@ -2,18 +2,15 @@ use std::fmt::Write; use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; -use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::transport::{TransportList, TransportStatus}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::output::Output; use crate::terminal::OckamColor; use crate::util::node_rpc; @@ -34,7 +31,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -43,14 +39,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_transports = async { @@ -62,9 +51,7 @@ async fn run_impl( let output_messages = vec![format!( "Listing TCP Connections on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -75,12 +62,10 @@ async fn run_impl( let list = opts.terminal.build_list( &transports.list, - &format!("TCP Connections on {}", node_name), + &format!("TCP Connections on {}", node.node_name()), &format!( "No TCP Connections found on {}", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) ), )?; diff --git a/implementations/rust/ockam/ockam_command/src/tcp/connection/show.rs b/implementations/rust/ockam/ockam_command/src/tcp/connection/show.rs index 71a11c88580..88b1ad49912 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/connection/show.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/connection/show.rs @@ -1,12 +1,11 @@ use clap::Args; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::transport::TransportStatus; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -28,7 +27,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)); } } @@ -37,9 +35,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let transport_status: TransportStatus = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs index 087cd206549..6b211285a31 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/create.rs @@ -1,6 +1,5 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; - use std::time::Duration; use clap::Args; @@ -13,8 +12,8 @@ use tracing::trace; use ockam::identity::Identifier; use ockam::Context; -use ockam_api::cli_state::{CliState, StateDirTrait}; - +use ockam_api::address::extract_address_value; +use ockam_api::cli_state::CliState; use ockam_api::nodes::models::portal::InletStatus; use ockam_api::nodes::service::portals::Inlets; use ockam_api::nodes::BackgroundNode; @@ -24,15 +23,11 @@ use ockam_core::Error; use ockam_multiaddr::proto::Project; use ockam_multiaddr::{MultiAddr, Protocol as _}; -use crate::node::{get_node_name, initialize_node_if_default}; - use crate::tcp::util::alias_parser; use crate::terminal::OckamColor; use crate::util::duration::duration_parser; use crate::util::parsers::socket_addr_parser; -use crate::util::{ - find_available_port, node_rpc, parse_node_name, port_is_free_guard, process_nodes_multiaddr, -}; +use crate::util::{find_available_port, node_rpc, port_is_free_guard, process_nodes_multiaddr}; use crate::{display_parse_logs, docs, fmt_log, fmt_ok, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -42,7 +37,7 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" #[command(after_long_help = docs::after_help(AFTER_LONG_HELP))] pub struct CreateCommand { /// Node on which to start the tcp inlet. - #[arg(long, display_order = 900, id = "NODE")] + #[arg(long, display_order = 900, id = "NODE", value_parser = extract_address_value)] at: Option, /// Address on which to accept tcp connections. @@ -85,7 +80,6 @@ fn default_to_addr() -> String { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.at); node_rpc(rpc, (opts, self)); } @@ -93,24 +87,33 @@ impl CreateCommand { MultiAddr::from_str(&self.to).unwrap() } - fn parse_args(mut self, opts: &CommandGlobalOpts) -> Result { - self.to = Self::parse_arg_to(&opts.state, self.to)?; + async fn parse_args(mut self, opts: &CommandGlobalOpts) -> Result { + let default_project_name = &opts + .state + .get_default_project() + .await + .ok() + .map(|p| p.name()); + + self.to = Self::parse_arg_to(&opts.state, self.to, default_project_name).await?; Ok(self) } - fn parse_arg_to(state: &CliState, to: impl Into) -> Result { + async fn parse_arg_to( + state: &CliState, + to: impl Into, + default_project_name: &Option, + ) -> Result { let mut to = to.into(); - - let default_project_name = || -> Result { - let default_project = state.projects.default().wrap_err( - "There is no default project defined. Please enroll or create a project.", - )?; - Ok(default_project.name().to_string()) - }; + let missing_project_error = + "There is no default project defined. Please enroll or create a project."; // Replace the placeholders in the default arg value if to.starts_with("/project/") { - to = to.replace("$PROJECT_NAME", &default_project_name()?); + let project_name = default_project_name + .clone() + .ok_or(miette!(missing_project_error))?; + to = to.replace("$PROJECT_NAME", &project_name); to = to.replace("$RELAY_NAME", "default"); } @@ -123,20 +126,24 @@ impl CreateCommand { if to.contains('/') { return Err(miette!("The relay name can't contain '/'")); } - let default_project_name = default_project_name()?; + let project_name = default_project_name + .clone() + .ok_or(miette!(missing_project_error))?; + MultiAddr::from_str(&format!( - "/project/{default_project_name}/service/forward_to_{to}/secure/api/service/outlet" + "/project/{}/service/forward_to_{to}/secure/api/service/outlet", + project_name )) .into_diagnostic() .wrap_err("Invalid address value or relay name")? } }; - Ok(process_nodes_multiaddr(&ma, state)?.to_string()) + Ok(process_nodes_multiaddr(&ma, state).await?.to_string()) } } async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> Result<()> { - let cmd = cmd.parse_args(&opts)?; + let cmd = cmd.parse_args(&opts).await?; opts.terminal.write_line(&fmt_log!( "Creating TCP Inlet at {}...\n", cmd.from @@ -145,10 +152,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> R ))?; display_parse_logs(&opts); - let node_name = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&node_name)?; - - let mut node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let mut node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; cmd.timeout.map(|t| node.set_timeout(t)); let is_finished: Mutex = Mutex::new(false); @@ -211,9 +215,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> R let progress_messages = vec![ format!( "Creating TCP Inlet on {}...", - &node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + &node.node_name().color(OckamColor::PrimaryResource.color()) ), format!( "Hosting TCP Socket at {}...", @@ -242,9 +244,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> R &cmd.from .to_string() .color(OckamColor::PrimaryResource.color()), - &node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + &node.node_name().color(OckamColor::PrimaryResource.color()) ) + &fmt_log!( "to the outlet at {}", &cmd.to @@ -261,30 +261,26 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand)) -> R #[cfg(test)] mod tests { + use miette::Result; + use super::*; - use ockam_api::cli_state::ProjectConfig; - #[test] - fn test_parse_arg_to() { - let state = CliState::test().unwrap(); + #[tokio::test] + async fn test_parse_arg_to() -> Result<()> { + let state = CliState::test().await?; + let default_project_name = Some("p1".to_string()); // Invalid values - CreateCommand::parse_arg_to(&state, "/alice/service").expect_err("Invalid protocol"); - CreateCommand::parse_arg_to(&state, "alice/forwarder").expect_err("Invalid protocol"); - CreateCommand::parse_arg_to( - &state, - "/project/my_project/service/forward_to_n1/secure/api/service/outlet", - ) - .expect_err("Project doesn't exist"); - - // Create necessary state - state - .projects - .create("p1", ProjectConfig::default()) - .unwrap(); + CreateCommand::parse_arg_to(&state, "/alice/service", &default_project_name) + .await + .expect_err("Invalid protocol"); + CreateCommand::parse_arg_to(&state, "alice/forwarder", &default_project_name) + .await + .expect_err("Invalid protocol"); // The placeholders are replaced in the default value - let res = CreateCommand::parse_arg_to(&state, default_to_addr()).unwrap(); + let res = + CreateCommand::parse_arg_to(&state, default_to_addr(), &default_project_name).await?; assert_eq!( res, "/project/p1/service/forward_to_default/secure/api/service/outlet" @@ -292,14 +288,15 @@ mod tests { // The user provides a full project route let addr = "/project/p1/service/forward_to_n1/secure/api/service/outlet"; - let res = CreateCommand::parse_arg_to(&state, addr).unwrap(); + let res = CreateCommand::parse_arg_to(&state, addr, &default_project_name).await?; assert_eq!(res, addr); // The user provides the name of the relay - let res = CreateCommand::parse_arg_to(&state, "alice").unwrap(); + let res = CreateCommand::parse_arg_to(&state, "alice", &default_project_name).await?; assert_eq!( res, "/project/p1/service/forward_to_alice/secure/api/service/outlet" ); + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs index def59da1cb4..39ba8e4c8ed 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/delete.rs @@ -10,10 +10,10 @@ use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use crate::fmt_ok; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::tcp::util::alias_parser; use crate::terminal::tui::DeleteCommandTui; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, fmt_warn, CommandGlobalOpts, Terminal, TerminalStream}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -37,7 +37,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -62,11 +61,7 @@ impl DeleteTui { opts: CommandGlobalOpts, cmd: DeleteCommand, ) -> miette::Result<()> { - let node_name = { - let name = get_node_name(&opts.state, &cmd.node_opts.at_node); - parse_node_name(&name)? - }; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let tui = Self { ctx, opts, @@ -132,13 +127,13 @@ impl DeleteCommandTui for DeleteTui { plain.push_str(&fmt_ok!( "TCP inlet with alias {} on Node {} has been deleted\n", item_name.light_magenta(), - node_name.light_magenta() + node_name.clone().light_magenta() )); } else { plain.push_str(&fmt_warn!( "Failed to delete TCP inlet with alias {} on Node {}\n", item_name.light_magenta(), - node_name.light_magenta() + node_name.clone().light_magenta() )); } } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs index 2e36fe2e397..c59e4cfe2ec 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/list.rs @@ -1,17 +1,15 @@ use clap::Args; use colorful::Colorful; -use miette::{miette, IntoDiagnostic}; +use miette::IntoDiagnostic; use tokio::sync::Mutex; use tokio::try_join; -use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::portal::InletList; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::terminal::OckamColor; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -31,7 +29,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node.at_node); node_rpc(run_impl, (opts, self)) } } @@ -40,14 +37,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node.at_node); - let node_name = extract_address_value(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_inlets = async { @@ -58,9 +48,7 @@ async fn run_impl( let output_messages = vec![format!( "Listing TCP Inlets on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -72,7 +60,7 @@ async fn run_impl( let plain = opts.terminal.build_list( &inlets.list, "Inlets", - &format!("No TCP Inlets found on {node_name}"), + &format!("No TCP Inlets found on {}", node.node_name()), )?; let json = serde_json::to_string_pretty(&inlets.list).into_diagnostic()?; opts.terminal diff --git a/implementations/rust/ockam/ockam_command/src/tcp/inlet/show.rs b/implementations/rust/ockam/ockam_command/src/tcp/inlet/show.rs index ae13ffe2617..5ba10af1608 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/inlet/show.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/inlet/show.rs @@ -8,11 +8,10 @@ use ockam_api::nodes::models::portal::InletStatus; use ockam_api::nodes::service::portals::Inlets; use ockam_api::nodes::BackgroundNode; -use crate::fmt_ok; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::tcp::util::alias_parser; -use crate::util::{node_rpc, parse_node_name}; -use crate::{docs, CommandGlobalOpts}; +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); @@ -34,7 +33,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -43,10 +41,7 @@ pub async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let inlet_status = node .show_inlet(&ctx, &cmd.alias) .await? diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/create.rs index 334fa9975ac..f808adc6556 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/create.rs @@ -1,12 +1,7 @@ -use crate::util::{node_rpc, parse_node_name}; -use crate::{docs, fmt_log, CommandGlobalOpts}; -use crate::{ - fmt_ok, - node::{get_node_name, initialize_node_if_default}, -}; use clap::Args; use colorful::Colorful; use miette::IntoDiagnostic; +use ockam_api::address::extract_address_value; use ockam_api::nodes::models::transport::{CreateTcpListener, TransportStatus}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; @@ -14,6 +9,10 @@ use ockam_multiaddr::proto::{DnsAddr, Tcp}; use ockam_multiaddr::MultiAddr; use ockam_node::Context; +use crate::util::node_rpc; +use crate::{docs, CommandGlobalOpts}; +use crate::{fmt_log, fmt_ok}; + const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); /// Create a TCP listener @@ -21,7 +20,7 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" #[command(after_long_help = docs::after_help(AFTER_LONG_HELP))] pub struct CreateCommand { /// Node at which to create the listener - #[arg(global = true, long, value_name = "NODE")] + #[arg(global = true, long, value_name = "NODE", value_parser = extract_address_value)] pub at: Option, /// Address for this listener (eg. 127.0.0.1:7000) @@ -30,7 +29,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.at); node_rpc(run_impl, (opts, self)) } } @@ -39,9 +37,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, CreateCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.at); - let node_name = parse_node_name(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let transport_status: TransportStatus = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/delete.rs index c4594b849e3..0dcac3b1484 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/delete.rs @@ -8,9 +8,7 @@ use ockam_api::nodes::models::transport::TransportStatus; use ockam_api::nodes::{models, BackgroundNode}; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::util::node_rpc; -use crate::util::parse_node_name; use crate::{docs, fmt_ok, node::NodeOpts, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -32,7 +30,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)); } } @@ -41,9 +38,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; // Check if there an TCP listener with the provided address exists let address = cmd.address; @@ -55,7 +50,8 @@ async fn run_impl( .found() .into_diagnostic()? .ok_or(miette!( - "TCP listener with address {address} was not found on Node {node_name}" + "TCP listener with address {address} was not found on Node {}", + node.node_name() ))?; // Proceed with the deletion @@ -70,9 +66,10 @@ async fn run_impl( opts.terminal .stdout() .plain(fmt_ok!( - "TCP listener with address {address} on Node {node_name} has been deleted" + "TCP listener with address {address} on Node {} has been deleted", + node.node_name() )) - .json(serde_json::json!({"node": node_name })) + .json(serde_json::json!({"node": node.node_name() })) .write_line() .unwrap(); } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs index c20713c32c7..d4670e20f4b 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/list.rs @@ -1,17 +1,15 @@ use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::transport::TransportList; use ockam_api::nodes::BackgroundNode; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::terminal::OckamColor; -use crate::util::{api, node_rpc, parse_node_name}; +use crate::util::{api, node_rpc}; use crate::{docs, CommandGlobalOpts}; const PREVIEW_TAG: &str = include_str!("../../static/preview_tag.txt"); @@ -29,7 +27,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(rpc, (opts, self)); } } @@ -39,14 +36,7 @@ async fn rpc(ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand)) -> mie } async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let get_transports = async { @@ -57,9 +47,7 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let output_messages = vec![format!( "Listing TCP Listeners on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -70,12 +58,10 @@ async fn run_impl(ctx: &Context, opts: CommandGlobalOpts, cmd: ListCommand) -> m let list = opts.terminal.build_list( &transports.list, - &format!("TCP Listeners on {}", node_name), + &format!("TCP Listeners on {}", node.node_name()), &format!( "No TCP Listeners found on {}", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) ), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/src/tcp/listener/show.rs b/implementations/rust/ockam/ockam_command/src/tcp/listener/show.rs index 8a4b8b7b4c4..52aaeb70c9c 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/listener/show.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/listener/show.rs @@ -2,12 +2,11 @@ use clap::Args; use indoc::formatdoc; use ockam::Context; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::transport::TransportStatus; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -29,7 +28,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)); } } @@ -38,9 +36,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let transport_status: TransportStatus = node .ask( &ctx, diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs index d691fb45f9d..3131c6b2742 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/create.rs @@ -9,12 +9,10 @@ use tokio::try_join; use ockam::Context; use ockam_abac::Resource; use ockam_api::address::extract_address_value; -use ockam_api::cli_state::{StateDirTrait, StateItemTrait}; use ockam_api::nodes::models::portal::{CreateOutlet, OutletStatus}; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::policy::{add_default_project_policy, has_policy}; use crate::tcp::util::alias_parser; use crate::terminal::OckamColor; @@ -30,11 +28,11 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" #[command(after_long_help = docs::after_help(AFTER_LONG_HELP))] pub struct CreateCommand { /// Node on which to start the tcp outlet. - #[arg(long, display_order = 900, id = "NODE")] + #[arg(long, display_order = 900, id = "NODE", value_parser = extract_address_value)] at: Option, /// Address of the tcp outlet. - #[arg(long, display_order = 901, id = "OUTLET_ADDRESS", default_value_t = default_from_addr())] + #[arg(long, display_order = 901, id = "OUTLET_ADDRESS", default_value_t = default_from_addr(), value_parser = extract_address_value)] from: String, /// TCP address to send raw tcp traffic. @@ -48,7 +46,6 @@ pub struct CreateCommand { impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.at); node_rpc(run_impl, (opts, self)) } } @@ -69,32 +66,19 @@ pub async fn run_impl( ))?; display_parse_logs(&opts); - let node_name = get_node_name(&opts.state, &cmd.at); - let node_name = extract_address_value(&node_name)?; - let project = opts - .state - .nodes - .get(&node_name)? - .config() - .setup() - .project - .to_owned(); + let node_name = opts.state.get_node_or_default(&cmd.at).await?.name(); + let project = opts.state.get_node_project(&node_name).await.ok(); let resource = Resource::new("tcp-outlet"); if let Some(p) = project { if !has_policy(&node_name, &ctx, &opts, &resource).await? { - add_default_project_policy(&node_name, &ctx, &opts, p, &resource).await?; + add_default_project_policy(&node_name, &ctx, &opts, p.id, &resource).await?; } } let is_finished: Mutex = Mutex::new(false); let send_req = async { - let payload = CreateOutlet::new( - cmd.to, - extract_address_value(&cmd.from)?.into(), - cmd.alias, - true, - ); + let payload = CreateOutlet::new(cmd.to, cmd.from.clone().into(), cmd.alias, true); let res = send_request(&ctx, &opts, payload, node_name.clone()).await; *is_finished.lock().await = true; res @@ -110,9 +94,7 @@ pub async fn run_impl( "Setting up TCP outlet worker...".to_string(), format!( "Hosting outlet service at {}...", - &cmd.from - .to_string() - .color(OckamColor::PrimaryResource.color()) + cmd.from.clone().color(OckamColor::PrimaryResource.color()) ), ]; @@ -131,8 +113,7 @@ pub async fn run_impl( &node_name .to_string() .color(OckamColor::PrimaryResource.color()), - format!("/service/{}", extract_address_value(&cmd.from)?) - .color(OckamColor::PrimaryResource.color()), + &cmd.from.color(OckamColor::PrimaryResource.color()), &cmd.to .to_string() .color(OckamColor::PrimaryResource.color()) @@ -150,8 +131,7 @@ pub async fn send_request( payload: CreateOutlet, to_node: impl Into>, ) -> crate::Result { - let node_name = get_node_name(&opts.state, &to_node.into()); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(ctx, &opts.state, &to_node.into()).await?; let req = Request::post("/node/outlet").body(payload); Ok(node.ask(ctx, req).await?) } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/delete.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/delete.rs index 96edfc27eb0..ab0d35bc255 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/delete.rs @@ -8,9 +8,9 @@ use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use crate::fmt_ok; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::tcp::util::alias_parser; -use crate::util::{node_rpc, parse_node_name}; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -34,7 +34,6 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -43,9 +42,7 @@ pub async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, DeleteCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = parse_node_name(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; // Check if there an outlet with the provided alias/name exists let alias = cmd.alias; @@ -54,7 +51,8 @@ pub async fn run_impl( .found() .into_diagnostic()? .ok_or(miette!( - "TCP outlet with alias {alias} was not found on Node {node_name}" + "TCP outlet with alias {alias} was not found on Node {}", + node.node_name() ))?; // Proceed with the deletion @@ -68,10 +66,11 @@ pub async fn run_impl( opts.terminal .stdout() .plain(fmt_ok!( - "TCP outlet with alias {alias} on Node {node_name} has been deleted" + "TCP outlet with alias {alias} on Node {} has been deleted", + node.node_name() )) .machine(&alias) - .json(serde_json::json!({ "alias": alias, "node": node_name })) + .json(serde_json::json!({ "alias": alias, "node": node.node_name() })) .write_line() .unwrap(); } diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/list.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/list.rs index 3122d8dc44f..ce15af7bc64 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/list.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/list.rs @@ -1,17 +1,14 @@ use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; -use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::portal::OutletList; use ockam_api::nodes::BackgroundNode; use ockam_core::api::Request; use ockam_node::Context; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::terminal::OckamColor; use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; @@ -31,7 +28,6 @@ pub struct ListCommand { impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -40,26 +36,19 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; let is_finished: Mutex = Mutex::new(false); let send_req = async { - let res = send_request(&ctx, &opts, node_name.clone()).await; + let res: OutletList = node.ask(&ctx, Request::get("/node/outlet")).await?; *is_finished.lock().await = true; - res + Ok(res) }; let output_messages = vec![format!( "Listing TCP Outlets on node {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -70,8 +59,8 @@ async fn run_impl( let list = opts.terminal.build_list( &outlets.list, - &format!("Outlets on Node {node_name}"), - &format!("No TCP Outlets found on node {node_name}."), + &format!("Outlets on Node {}", node.node_name()), + &format!("No TCP Outlets found on node {}.", node.node_name()), )?; let json: Vec<_> = outlets .list @@ -93,13 +82,3 @@ async fn run_impl( Ok(()) } - -pub async fn send_request( - ctx: &Context, - opts: &CommandGlobalOpts, - to_node: impl Into>, -) -> crate::Result { - let node_name = get_node_name(&opts.state, &to_node.into()); - let node = BackgroundNode::create(ctx, &opts.state, &node_name).await?; - Ok(node.ask(ctx, Request::get("/node/outlet")).await?) -} diff --git a/implementations/rust/ockam/ockam_command/src/tcp/outlet/show.rs b/implementations/rust/ockam/ockam_command/src/tcp/outlet/show.rs index fa6712df3aa..b2f02436f1f 100644 --- a/implementations/rust/ockam/ockam_command/src/tcp/outlet/show.rs +++ b/implementations/rust/ockam/ockam_command/src/tcp/outlet/show.rs @@ -6,14 +6,13 @@ use miette::miette; use serde::Serialize; use ockam::{route, Context}; -use ockam_api::address::extract_address_value; use ockam_api::nodes::models::portal::OutletStatus; use ockam_api::nodes::BackgroundNode; use ockam_api::route_to_multiaddr; use ockam_core::api::Request; use ockam_multiaddr::MultiAddr; -use crate::node::{get_node_name, initialize_node_if_default, NodeOpts}; +use crate::node::NodeOpts; use crate::output::Output; use crate::tcp::util::alias_parser; use crate::util::node_rpc; @@ -40,7 +39,6 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.node_opts.at_node); node_rpc(run_impl, (opts, self)) } } @@ -67,10 +65,10 @@ pub async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ShowCommand), ) -> miette::Result<()> { - let node_name = get_node_name(&opts.state, &cmd.node_opts.at_node); - let node_name = extract_address_value(&node_name)?; - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; - let outlet_status: OutletStatus = node.ask(&ctx, make_api_request(cmd)?).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.node_opts.at_node).await?; + let outlet_status: OutletStatus = node + .ask(&ctx, Request::get(format!("/node/outlet/{}", cmd.alias))) + .await?; let info = OutletInformation { alias: outlet_status.alias, addr: route_to_multiaddr(&route![outlet_status.worker_addr.to_string()]) @@ -85,10 +83,3 @@ pub async fn run_impl( .write_line()?; Ok(()) } - -/// Construct a request to show a tcp outlet -fn make_api_request(cmd: ShowCommand) -> Result { - let alias = cmd.alias; - let request = Request::get(format!("/node/outlet/{alias}")); - Ok(request) -} diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/create.rs b/implementations/rust/ockam/ockam_command/src/trust_context/create.rs index 7a2ae2a13d1..7774e56595f 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/create.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/create.rs @@ -1,9 +1,15 @@ -use crate::util::local_cmd; -use crate::{docs, util::api::TrustContextOpts, CommandGlobalOpts}; use clap::Args; use indoc::formatdoc; -use miette::{miette, IntoDiagnostic}; -use ockam_api::cli_state::{random_name, StateDirTrait}; +use miette::IntoDiagnostic; + +use ockam::identity::Identity; +use ockam_api::cli_state::random_name; +use ockam_core::env::FromString; +use ockam_multiaddr::MultiAddr; +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/create/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt"); @@ -11,66 +17,81 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create a trust context #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = false, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +arg_required_else_help = false, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct CreateCommand { /// The name of the trust context to create #[arg(default_value_t = random_name())] name: String, + /// The id of the trust context to create + #[arg(long)] + id: Option, + /// Create a trust context from a credential #[arg(long)] credential: Option, - #[command(flatten)] - trust_context_opts: TrustContextOpts, + /// Create a trust context from an authority + #[arg(long)] + authority_identity: Option, + + /// Create a trust context from an authority + #[arg(long)] + authority_route: Option, } impl CreateCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: CreateCommand) -> miette::Result<()> { - let config = cmd - .trust_context_opts - .to_config(&opts.state)? - .with_credential_name(cmd.credential.as_ref()) - .use_default_trust_context(false) - .build(); +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, CreateCommand), +) -> miette::Result<()> { + let authority = match &cmd.authority_identity { + None => None, + Some(identity) => Some(Identity::create(identity).await.into_diagnostic()?), + }; + let authority_route = cmd + .authority_route + .map(|r| MultiAddr::from_string(&r).into_diagnostic()) + .transpose()?; - if let Some(c) = config { - opts.state.trust_contexts.create(&cmd.name, c.clone())?; + let trust_context = opts + .state + .create_trust_context( + Some(cmd.name.clone()), + cmd.id.clone(), + cmd.credential, + authority, + authority_route, + ) + .await?; - let auth = if let Ok(auth) = c.authority() { - auth.identity_str() - } else { - "None" - }; + let authority = trust_context + .authority_identity() + .await + .into_diagnostic()? + .map(|i| i.change_history().export_as_string().unwrap()) + .unwrap_or("None".to_string()); - let output = formatdoc!( - r#" + let output = formatdoc!( + r#" Trust Context: Name: {} ID: {} Authority: {} "#, - cmd.name, - c.id(), - auth - ); - - opts.terminal - .stdout() - .plain(output) - .json(serde_json::to_string_pretty(&c).into_diagnostic()?) - .write_line()?; - } else { - return Err(miette!("Unable to create trust context")); - } + cmd.name, + trust_context.trust_context_id(), + authority + ); + opts.terminal.stdout().plain(output).write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/default.rs b/implementations/rust/ockam/ockam_command/src/trust_context/default.rs index 2108917ebc7..4e30919edae 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/default.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/default.rs @@ -1,9 +1,9 @@ -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; const LONG_ABOUT: &str = include_str!("./static/default/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt"); @@ -22,21 +22,23 @@ pub struct DefaultCommand { impl DefaultCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DefaultCommand) -> miette::Result<()> { - let DefaultCommand { name } = cmd; - let state = opts.state.trust_contexts; - let tc = state.get(&name)?; +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, DefaultCommand), +) -> miette::Result<()> { + let name = cmd.name; + let default_trust_context = opts.state.get_default_trust_context().await?; // If it exists, warn the user and exit - if state.is_default(tc.name())? { + if default_trust_context.name() == name { Err(miette!("The trust context '{name}' is already the default")) } // Otherwise, set it as default else { - state.set_default(tc.name())?; + opts.state.set_default_trust_context(&name).await?; opts.terminal .stdout() .plain(fmt_ok!("The trust context '{name}' is now the default")) diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/delete.rs b/implementations/rust/ockam/ockam_command/src/trust_context/delete.rs index e293f6e6cd9..b100759d9d0 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/delete.rs @@ -1,8 +1,9 @@ -use crate::util::local_cmd; -use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; + +use crate::util::node_rpc; +use crate::{docs, fmt_ok, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/delete/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt"); @@ -10,9 +11,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/delete/after_long_help.txt" /// Delete a trust context #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = false, - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +arg_required_else_help = false, +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct DeleteCommand { /// Name of the trust context @@ -25,25 +26,26 @@ pub struct DeleteCommand { impl DeleteCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DeleteCommand) -> miette::Result<()> { +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, DeleteCommand), +) -> miette::Result<()> { if opts.terminal.confirmed_with_flag_or_prompt( cmd.yes, "Are you sure you want to delete this trust context?", )? { - let name = cmd.name; - let state = opts.state.trust_contexts; - state.get(&name)?; - state.delete(&name)?; + let name = &cmd.name; + opts.state.delete_trust_context(name).await?; opts.terminal .stdout() .plain(fmt_ok!( "The trust context with name '{name}' has been deleted" )) - .machine(&name) + .machine(name) .json(serde_json::json!({ "name": &name })) .write_line()?; } diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/list.rs b/implementations/rust/ockam/ockam_command/src/trust_context/list.rs index e6e85c4fc5b..aecf057b8bf 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/list.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/list.rs @@ -1,8 +1,8 @@ use clap::Args; use miette::miette; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); @@ -12,26 +12,26 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List trust contexts #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand; impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts)); + node_rpc(run_impl, opts); } } -fn run_impl(opts: CommandGlobalOpts) -> miette::Result<()> { - let states = opts.state.trust_contexts.list()?; - if states.is_empty() { +async fn run_impl(_ctx: Context, opts: CommandGlobalOpts) -> miette::Result<()> { + let trust_contexts = opts.state.get_trust_contexts().await?; + if trust_contexts.is_empty() { return Err(miette!("No trust contexts registered on this system!")); } let plain_output = { let mut output = String::new(); - for (idx, tc) in states.iter().enumerate() { + for (idx, tc) in trust_contexts.iter().enumerate() { output.push_str(&format!("Trust context[{idx}]:")); for line in tc.to_string().lines() { output.push_str(&format!("{:2}{}\n", "", line)); diff --git a/implementations/rust/ockam/ockam_command/src/trust_context/show.rs b/implementations/rust/ockam/ockam_command/src/trust_context/show.rs index 570ebed49c1..ba567b0e730 100644 --- a/implementations/rust/ockam/ockam_command/src/trust_context/show.rs +++ b/implementations/rust/ockam/ockam_command/src/trust_context/show.rs @@ -1,7 +1,7 @@ use clap::Args; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::{docs, CommandGlobalOpts}; const LONG_ABOUT: &str = include_str!("./static/show/long_about.txt"); @@ -22,18 +22,18 @@ pub struct ShowCommand { impl ShowCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: ShowCommand) -> miette::Result<()> { - let name = cmd - .name - .unwrap_or(opts.state.trust_contexts.default()?.name().to_string()); - let state = opts.state.trust_contexts.get(name)?; +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, ShowCommand), +) -> miette::Result<()> { + let trust_context = opts.state.get_trust_context_or_default(&cmd.name).await?; let plain_output = { let mut output = "Trust context:".to_string(); - for line in state.to_string().lines() { + for line in trust_context.to_string().lines() { output.push_str(&format!("{:2}{}\n", "", line)); } output diff --git a/implementations/rust/ockam/ockam_command/src/util/api.rs b/implementations/rust/ockam/ockam_command/src/util/api.rs index 381f907be5d..86b86a14c2a 100644 --- a/implementations/rust/ockam/ockam_command/src/util/api.rs +++ b/implementations/rust/ockam/ockam_command/src/util/api.rs @@ -1,7 +1,4 @@ //! API shim to make it nicer to interact with the ockam messaging API - -use std::path::PathBuf; - use clap::Args; use miette::miette; // TODO: maybe we can remove this cross-dependency inside the CLI? @@ -9,15 +6,13 @@ use minicbor::Decoder; use regex::Regex; use ockam::identity::Identifier; -use ockam_api::cli_state::CliState; use ockam_api::nodes::models::flow_controls::AddConsumer; use ockam_api::nodes::models::services::{ StartAuthenticatedServiceRequest, StartAuthenticatorRequest, StartCredentialsService, StartHopServiceRequest, StartOktaIdentityProviderRequest, }; +use ockam_api::nodes::service::default_address::DefaultAddress; use ockam_api::nodes::*; -use ockam_api::trust_context::TrustContextConfigBuilder; -use ockam_api::DefaultAddress; use ockam_core::api::Request; use ockam_core::api::ResponseHeader; use ockam_core::flow_control::FlowControlId; @@ -204,10 +199,6 @@ pub struct CloudOpts { #[derive(Clone, Debug, Args, Default)] pub struct TrustContextOpts { - /// Project config file - #[arg(global = true, long = "project-path", value_name = "PROJECT_JSON_PATH")] - pub project_path: Option, - /// Trust Context config file #[arg( global = true, @@ -217,24 +208,12 @@ pub struct TrustContextOpts { pub trust_context: Option, #[arg(global = true, long = "project", value_name = "PROJECT_NAME")] - pub project: Option, + pub project_name: Option, } impl TrustContextOpts { - pub fn to_config(&self, cli_state: &CliState) -> Result { - let trust_context = match &self.trust_context { - Some(tc) => Some(cli_state.trust_contexts.read_config_from_path(tc)?), - None => None, - }; - Ok(TrustContextConfigBuilder { - cli_state: cli_state.clone(), - project_path: self.project_path.clone(), - trust_context, - project: self.project.clone(), - authority_identity: None, - credential_name: None, - use_default_trust_context: true, - }) + pub fn project_name(&self) -> Option { + self.project_name.clone() } } diff --git a/implementations/rust/ockam/ockam_command/src/util/mod.rs b/implementations/rust/ockam/ockam_command/src/util/mod.rs index 6fbe1c3b3ed..8313099ef49 100644 --- a/implementations/rust/ockam/ockam_command/src/util/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/util/mod.rs @@ -1,7 +1,6 @@ use std::{ net::{SocketAddr, TcpListener}, path::Path, - str::FromStr, }; use miette::Context as _; @@ -9,15 +8,13 @@ use miette::{miette, IntoDiagnostic}; use tracing::error; use ockam::{Address, Context, NodeBuilder}; -use ockam_api::cli_state::{CliState, StateDirTrait, StateItemTrait}; +use ockam_api::cli_state::CliState; use ockam_api::config::lookup::{InternetAddress, LookupMeta}; use ockam_core::DenyAll; use ockam_multiaddr::proto::{DnsAddr, Ip4, Ip6, Project, Space, Tcp}; -use ockam_multiaddr::{ - proto::{self, Node}, - MultiAddr, Protocol, -}; +use ockam_multiaddr::{proto::Node, MultiAddr, Protocol}; +use crate::error::Error; use crate::Result; pub mod api; @@ -152,43 +149,15 @@ pub fn print_path(p: &Path) -> String { p.to_str().unwrap_or("").to_string() } -/// Parses a node's input string for its name in case it's a `MultiAddr` string. -/// -/// Ensures that the node's name will be returned if the input string is a `MultiAddr` of the `node` type -/// Examples: `n1` or `/node/n1` returns `n1`; `/project/p1` or `/tcp/n2` returns an error message. -pub fn parse_node_name(input: &str) -> Result { - if input.is_empty() { - return Err(miette!("Empty address in node name argument").into()); - } - // Node name was passed as "n1", for example - if !input.contains('/') { - return Ok(input.to_string()); - } - // Input has "/", so we process it as a MultiAddr - let maddr = MultiAddr::from_str(input) - .into_diagnostic() - .wrap_err("Invalid format for node name argument")?; - let err_message = String::from("A node MultiAddr must follow the format /node/"); - if let Some(p) = maddr.iter().next() { - if p.code() == proto::Node::CODE { - let node_name = p - .cast::() - .ok_or(miette!("Failed to parse the 'node' protocol"))? - .to_string(); - if !node_name.is_empty() { - return Ok(node_name); - } - } - } - Err(miette!(err_message).into()) -} - /// Replace the node's name with its address or leave it if it's another type of address. /// /// Example: /// if n1 has address of 127.0.0.1:1234 /// `/node/n1` -> `/ip4/127.0.0.1/tcp/1234` -pub fn process_nodes_multiaddr(addr: &MultiAddr, cli_state: &CliState) -> crate::Result { +pub async fn process_nodes_multiaddr( + addr: &MultiAddr, + cli_state: &CliState, +) -> crate::Result { let mut processed_addr = MultiAddr::default(); for proto in addr.iter() { match proto.code() { @@ -196,9 +165,8 @@ pub fn process_nodes_multiaddr(addr: &MultiAddr, cli_state: &CliState) -> crate: let alias = proto .cast::() .ok_or_else(|| miette!("Invalid node address protocol"))?; - let node_state = cli_state.nodes.get(alias.to_string())?; - let node_setup = node_state.config().setup(); - let addr = node_setup.api_transport()?.maddr()?; + let node_info = cli_state.get_node(&alias).await?; + let addr = node_info.tcp_listener_multi_address()?; processed_addr.try_extend(&addr)? } _ => processed_addr.push_back_value(&proto)?, @@ -210,7 +178,7 @@ pub fn process_nodes_multiaddr(addr: &MultiAddr, cli_state: &CliState) -> crate: /// Go through a multiaddr and remove all instances of /// `/node/` out of it and replaces it with a fully /// qualified address to the target -pub fn clean_nodes_multiaddr( +pub async fn clean_nodes_multiaddr( input: &MultiAddr, cli_state: &CliState, ) -> Result<(MultiAddr, LookupMeta)> { @@ -221,10 +189,14 @@ pub fn clean_nodes_multiaddr( match p.code() { Node::CODE => { let alias = p.cast::().expect("Failed to parse node name"); - let node_state = cli_state.nodes.get(alias.to_string())?; - let node_setup = node_state.config().setup(); - let addr = &node_setup.api_transport()?.addr; - match addr { + let node_info = cli_state.get_node(&alias).await?; + let addr = node_info + .tcp_listener_address() + .ok_or(Error::new_internal_error( + "No transport API has been set on the node", + "", + ))?; + match &addr { InternetAddress::Dns(dns, _) => new_ma.push_back(DnsAddr::new(dns))?, InternetAddress::V4(v4) => new_ma.push_back(Ip4(*v4.ip()))?, InternetAddress::V6(v6) => new_ma.push_back(Ip6(*v6.ip()))?, @@ -269,137 +241,44 @@ pub fn is_tty(s: S) -> bool { s.is_terminal() } -pub fn is_enrolled_guard(cli_state: &CliState, identity_name: Option<&str>) -> miette::Result<()> { - if !cli_state - .identities - .get_or_default(identity_name) - .map(|s| s.is_enrolled()) - .unwrap_or(false) - { - return Err(miette!( - "Please enroll using 'ockam enroll' before using this command" - )); - } - Ok(()) -} - #[cfg(test)] mod tests { - use ockam_api::address::extract_address_value; - use ockam_api::cli_state; - use ockam_api::cli_state::identities::IdentityConfig; - use ockam_api::cli_state::traits::StateDirTrait; - use ockam_api::cli_state::{NodeConfig, VaultConfig}; - use ockam_api::nodes::models::transport::{CreateTransportJson, TransportMode, TransportType}; + use std::str::FromStr; use super::*; - #[test] - fn test_parse_node_name() { - let test_cases = vec![ - ("", Err(())), - ("test", Ok("test")), - ("/test", Err(())), - ("test/", Err(())), - ("/node", Err(())), - ("/node/", Err(())), - ("/node/n1", Ok("n1")), - ("/service/s1", Err(())), - ("/project/p1", Err(())), - ("/randomprotocol/rp1", Err(())), - ("/node/n1/tcp", Err(())), - ("/node/n1/test", Err(())), - ("/node/n1/tcp/22", Ok("n1")), - ]; - for (input, expected) in test_cases { - if let Ok(addr) = expected { - assert_eq!(parse_node_name(input).unwrap(), addr); - } else { - assert!(parse_node_name(input).is_err()); - } - } - } - - #[test] - fn test_extract_address_value() { - let test_cases = vec![ - ("", Err(())), - ("test", Ok("test")), - ("/test", Err(())), - ("test/", Err(())), - ("/node", Err(())), - ("/node/", Err(())), - ("/node/n1", Ok("n1")), - ("/service/s1", Ok("s1")), - ("/project/p1", Ok("p1")), - ("/randomprotocol/rp1", Err(())), - ("/node/n1/tcp", Err(())), - ("/node/n1/test", Err(())), - ("/node/n1/tcp/22", Ok("n1")), - ]; - for (input, expected) in test_cases { - if let Ok(addr) = expected { - assert_eq!(extract_address_value(input).unwrap(), addr); - } else { - assert!(extract_address_value(input).is_err()); - } - } - } - #[ockam_macros::test(crate = "ockam")] async fn test_process_multi_addr(ctx: &mut Context) -> ockam::Result<()> { - let cli_state = CliState::test()?; + let cli_state = CliState::test().await?; - let v_name = cli_state::random_name(); - let v_config = VaultConfig::default(); - cli_state.vaults.create_async(&v_name, v_config).await?; - let v = cli_state.vaults.get(&v_name)?.get().await?; - let idt = cli_state - .get_identities(v) - .await - .unwrap() - .identities_creation() - .create_identity() - .await?; - let idt_config = IdentityConfig::new(&idt).await; - cli_state - .identities - .create(cli_state::random_name(), idt_config)?; + cli_state.create_node("n1").await?; - let n_state = cli_state - .nodes - .create("n1", NodeConfig::try_from(&cli_state)?)?; - n_state.set_setup(&n_state.config().setup_mut().set_api_transport( - CreateTransportJson::new(TransportType::Tcp, TransportMode::Listen, "127.0.0.0:4000")?, - ))?; + cli_state + .set_tcp_listener_address("n1", "127.0.0.0:4000".to_string()) + .await?; let test_cases = vec![ ( - MultiAddr::from_str("/node/n1").unwrap(), + MultiAddr::from_str("/node/n1")?, Ok("/ip4/127.0.0.0/tcp/4000"), ), + (MultiAddr::from_str("/project/p1")?, Ok("/project/p1")), + (MultiAddr::from_str("/service/s1")?, Ok("/service/s1")), ( - MultiAddr::from_str("/project/p1").unwrap(), - Ok("/project/p1"), - ), - ( - MultiAddr::from_str("/service/s1").unwrap(), - Ok("/service/s1"), - ), - ( - MultiAddr::from_str("/project/p1/node/n1/service/echo").unwrap(), + MultiAddr::from_str("/project/p1/node/n1/service/echo")?, Ok("/project/p1/ip4/127.0.0.0/tcp/4000/service/echo"), ), - (MultiAddr::from_str("/node/n2").unwrap(), Err(())), + (MultiAddr::from_str("/node/n2")?, Err(())), ]; for (ma, expected) in test_cases { if let Ok(addr) = expected { let result = process_nodes_multiaddr(&ma, &cli_state) + .await .unwrap() .to_string(); assert_eq!(result, addr); } else { - assert!(process_nodes_multiaddr(&ma, &cli_state).is_err()); + assert!(process_nodes_multiaddr(&ma, &cli_state).await.is_err()); } } diff --git a/implementations/rust/ockam/ockam_command/src/util/parsers.rs b/implementations/rust/ockam/ockam_command/src/util/parsers.rs index 0605d12c48a..ef8a6528aec 100644 --- a/implementations/rust/ockam/ockam_command/src/util/parsers.rs +++ b/implementations/rust/ockam/ockam_command/src/util/parsers.rs @@ -3,9 +3,12 @@ use std::str::FromStr; use miette::miette; -use ockam::identity::Identifier; +use ockam::identity::{Identifier, Identity}; +use ockam_api::config::lookup::InternetAddress; +use ockam_multiaddr::MultiAddr; use ockam_transport_tcp::resolve_peer; +use crate::util::api; use crate::Result; /// Helper function for parsing a socket from user input @@ -24,12 +27,45 @@ pub(crate) fn socket_addr_parser(input: &str) -> Result { .map_err(|e| miette!("cannot parse the address {address} as a socket address: {e}"))?) } -/// Helper fn for parsing an identity from user input by using +/// Helper fn for parsing an identifier from user input by using /// [`ockam_identity::Identifier::from_str()`] pub(crate) fn identity_identifier_parser(input: &str) -> Result { Identifier::from_str(input).map_err(|_| miette!("Invalid identity identifier: {input}").into()) } +/// Helper fn for parsing an identity from user input by using +/// [`ockam_identity::Identity::create()`] +pub(crate) fn identity_parser(input: &str) -> Result { + futures::executor::block_on(async { + Identity::create(input) + .await + .map_err(|_| miette!("Invalid identity: {input}").into()) + }) +} + +/// Helper fn for parsing a MultiAddr from user input by using +/// [`ockam_multiaddr::MultiAddr::from_str()`] +pub(crate) fn multiaddr_parser(input: &str) -> Result { + MultiAddr::from_str(input).map_err(|_| miette!("Invalid multiaddr: {input}").into()) +} + +/// Helper fn for parsing an InternetAddress from user input by using +/// [`InternetAddress::new()`] +pub(crate) fn internet_address_parser(input: &str) -> Result { + InternetAddress::new(input).ok_or_else(|| miette!("Invalid address: {input}").into()) +} + +pub(crate) fn validate_project_name(s: &str) -> Result { + match api::validate_cloud_resource_name(s) { + Ok(_) => Ok(s.to_string()), + Err(_e)=> Err(miette!( + "project name can contain only alphanumeric characters and the '-', '_' and '.' separators. \ + Separators must occur between alphanumeric characters. This implies that separators can't \ + occur at the start or end of the name, nor they can occur in sequence.", + ).into()), + } +} + #[cfg(test)] mod tests { use std::net::Ipv6Addr; diff --git a/implementations/rust/ockam/ockam_command/src/vault/attach_key.rs b/implementations/rust/ockam/ockam_command/src/vault/attach_key.rs new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/src/vault/attach_key.rs @@ -0,0 +1 @@ + diff --git a/implementations/rust/ockam/ockam_command/src/vault/create.rs b/implementations/rust/ockam/ockam_command/src/vault/create.rs index 41140265882..23a5845e0ff 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/create.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/create.rs @@ -2,9 +2,7 @@ use clap::Args; use colorful::Colorful; use ockam::Context; -use ockam_api::cli_state; use ockam_api::cli_state::random_name; -use ockam_api::cli_state::traits::StateDirTrait; use crate::util::node_rpc; use crate::{docs, fmt_info, fmt_ok, CommandGlobalOpts}; @@ -15,8 +13,8 @@ const AFTER_LONG_HELP: &str = include_str!("./static/create/after_long_help.txt" /// Create a vault #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct CreateCommand { #[arg(hide_default_value = true, default_value_t = random_name())] @@ -41,23 +39,22 @@ async fn run_impl( opts: CommandGlobalOpts, cmd: CreateCommand, ) -> miette::Result<()> { - let CreateCommand { name, aws_kms, .. } = cmd; - let config = cli_state::VaultConfig::new(aws_kms)?; - if opts.state.vaults.is_empty()? { + if opts.state.get_named_vaults().await?.is_empty() { opts.terminal.write_line(&fmt_info!( "This is the first vault to be created in this environment. It will be set as the default vault" ))?; } - opts.state - .vaults - .create_async(&name, config.clone()) - .await?; + if cmd.aws_kms { + opts.state.create_kms_vault(&cmd.name).await?; + } else { + opts.state.create_named_vault(&cmd.name).await?; + } opts.terminal .stdout() - .plain(fmt_ok!("Vault created with name '{name}'!")) - .machine(&name) - .json(serde_json::json!({ "name": &name })) + .plain(fmt_ok!("Vault created with name '{}'!", &cmd.name)) + .machine(&cmd.name) + .json(serde_json::json!({ "name": &cmd.name })) .write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/vault/default.rs b/implementations/rust/ockam/ockam_command/src/vault/default.rs index e67cffad88a..375bdc1e03c 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/default.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/default.rs @@ -1,9 +1,9 @@ -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::{docs, fmt_ok, CommandGlobalOpts}; use clap::Args; use colorful::Colorful; use miette::miette; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; const LONG_ABOUT: &str = include_str!("./static/default/long_about.txt"); const AFTER_LONG_HELP: &str = include_str!("./static/default/after_long_help.txt"); @@ -21,26 +21,33 @@ pub struct DefaultCommand { impl DefaultCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts, self)); + node_rpc(run_impl, (opts, self)); } } -fn run_impl(opts: CommandGlobalOpts, cmd: DefaultCommand) -> miette::Result<()> { - let DefaultCommand { name } = cmd; - let state = opts.state.vaults; - let v = state.get(&name)?; - // If it exists, warn the user and exit - if state.is_default(v.name())? { - Err(miette!("The vault '{}' is already the default", name)) +async fn run_impl( + _ctx: Context, + (opts, cmd): (CommandGlobalOpts, DefaultCommand), +) -> miette::Result<()> { + // If the vault is already the default vault, warn the user and exit + if opts + .state + .get_named_vault(&cmd.name) + .await + .ok() + .map(|v| v.is_default()) + .unwrap_or(false) + { + Err(miette!("The vault '{}' is already the default", &cmd.name)) } // Otherwise, set it as default else { - state.set_default(v.name())?; + opts.state.set_default_vault(&cmd.name).await?; opts.terminal .stdout() - .plain(fmt_ok!("The vault '{name}' is now the default")) - .machine(&name) - .json(serde_json::json!({ "name": name })) + .plain(fmt_ok!("The vault '{}' is now the default", &cmd.name)) + .machine(&cmd.name) + .json(serde_json::json!({ "name": cmd.name })) .write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/vault/delete.rs b/implementations/rust/ockam/ockam_command/src/vault/delete.rs index 7ac6516257a..d7b37440315 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/delete.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/delete.rs @@ -3,7 +3,6 @@ use colorful::Colorful; use console::Term; use ockam::Context; -use ockam_api::cli_state::traits::StateDirTrait; use crate::terminal::tui::DeleteCommandTui; use crate::util::node_rpc; @@ -84,15 +83,22 @@ impl DeleteCommandTui for DeleteTui { .cmd .name .clone() - .unwrap_or(self.opts.state.vaults.default()?.name().to_string())) + .unwrap_or(self.opts.state.get_default_named_vault().await?.name())) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.vaults.list_items_names()?) + Ok(self + .opts + .state + .get_named_vaults() + .await? + .iter() + .map(|v| v.name()) + .collect()) } async fn delete_single(&self, item_name: &str) -> miette::Result<()> { - self.opts.state.vaults.delete(item_name)?; + self.opts.state.delete_named_vault(item_name).await?; self.terminal() .stdout() .plain(fmt_ok!("Vault with name '{item_name}' has been deleted")) @@ -104,16 +110,20 @@ impl DeleteCommandTui for DeleteTui { } async fn delete_multiple(&self, selected_items_names: Vec) -> miette::Result<()> { - let plain = selected_items_names - .iter() - .map(|name| { - if self.opts.state.vaults.delete(name).is_ok() { - fmt_ok!("Vault '{name}' deleted\n") - } else { - fmt_warn!("Failed to delete vault '{name}'\n") - } - }) - .collect::(); + let mut plain = String::new(); + for name in selected_items_names { + if self + .opts + .state + .delete_named_vault(name.as_ref()) + .await + .is_ok() + { + plain.push_str(&fmt_ok!("Vault '{name}' deleted\n")) + } else { + plain.push_str(&fmt_warn!("Failed to delete vault '{name}'\n")) + } + } self.terminal().stdout().plain(plain).write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/vault/list.rs b/implementations/rust/ockam/ockam_command/src/vault/list.rs index 01885fd407f..7726b3368c9 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/list.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/list.rs @@ -1,9 +1,9 @@ -use crate::util::local_cmd; +use crate::util::node_rpc; use crate::vault::util::VaultOutput; use crate::{docs, CommandGlobalOpts}; use clap::Args; use miette::IntoDiagnostic; -use ockam_api::cli_state::traits::StateDirTrait; +use ockam_node::Context; const LONG_ABOUT: &str = include_str!("./static/list/long_about.txt"); const PREVIEW_TAG: &str = include_str!("../static/preview_tag.txt"); @@ -20,17 +20,17 @@ pub struct ListCommand; impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - local_cmd(run_impl(opts)); + node_rpc(run_impl, opts); } } -fn run_impl(opts: CommandGlobalOpts) -> miette::Result<()> { +async fn run_impl(_ctx: Context, opts: CommandGlobalOpts) -> miette::Result<()> { let vaults = opts .state - .vaults - .list()? + .get_named_vaults() + .await? .into_iter() - .map(|v| VaultOutput::new(&v, opts.state.vaults.is_default(v.name()).unwrap_or(false))) + .map(|v| VaultOutput::new(&v)) .collect::>(); let plain = opts .terminal diff --git a/implementations/rust/ockam/ockam_command/src/vault/mod.rs b/implementations/rust/ockam/ockam_command/src/vault/mod.rs index a04813a619a..85ea75b8528 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/mod.rs @@ -13,17 +13,15 @@ use crate::vault::show::ShowCommand; use crate::{docs, CommandGlobalOpts}; use clap::{Args, Subcommand}; -use ockam_api::cli_state::traits::StateDirTrait; -use ockam_api::cli_state::CliState; const LONG_ABOUT: &str = include_str!("./static/long_about.txt"); /// Manage Vaults #[derive(Clone, Debug, Args)] #[command( - arg_required_else_help = true, - subcommand_required = true, - long_about = docs::about(LONG_ABOUT), +arg_required_else_help = true, +subcommand_required = true, +long_about = docs::about(LONG_ABOUT), )] pub struct VaultCommand { #[command(subcommand)] @@ -50,10 +48,3 @@ impl VaultCommand { } } } - -pub fn default_vault_name(cli_state: &CliState) -> String { - cli_state - .vaults - .default() - .map_or("default".to_string(), |v| v.name().to_string()) -} diff --git a/implementations/rust/ockam/ockam_command/src/vault/show.rs b/implementations/rust/ockam/ockam_command/src/vault/show.rs index 0c8c6f360c2..33a2dac1c67 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/show.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/show.rs @@ -2,7 +2,6 @@ use clap::Args; use console::Term; use miette::IntoDiagnostic; -use ockam_api::cli_state::traits::StateDirTrait; use ockam_node::Context; use crate::output::Output; @@ -18,9 +17,9 @@ const AFTER_LONG_HELP: &str = include_str!("./static/show/after_long_help.txt"); /// Show the details of a vault #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ShowCommand { /// Name of the vault @@ -71,23 +70,27 @@ impl ShowCommandTui for ShowTui { Ok(self .vault_name .clone() - .unwrap_or(self.opts.state.vaults.default()?.name().to_string())) + .unwrap_or(self.opts.state.get_default_named_vault().await?.name())) } async fn list_items_names(&self) -> miette::Result> { - Ok(self.opts.state.vaults.list_items_names()?) + Ok(self + .opts + .state + .get_named_vaults() + .await? + .iter() + .map(|v| v.name()) + .collect()) } async fn show_single(&self, item_name: &str) -> miette::Result<()> { - let vault = VaultOutput::new( - &self.opts.state.vaults.get(item_name)?, - self.opts.state.vaults.is_default(item_name)?, - ); + let vault = VaultOutput::new(&self.opts.state.get_named_vault(item_name).await?); self.terminal() .stdout() .plain(vault.output()?) .json(serde_json::to_string(&vault).into_diagnostic()?) - .machine(&vault.name) + .machine(vault.name()) .write_line()?; Ok(()) } @@ -96,16 +99,11 @@ impl ShowCommandTui for ShowTui { let filtered = self .opts .state - .vaults - .list()? + .get_named_vaults() + .await? .into_iter() - .map(|v| { - VaultOutput::new( - &v, - self.opts.state.vaults.is_default(v.name()).unwrap_or(false), - ) - }) - .filter(|v| items_names.contains(&v.name)) + .map(|v| VaultOutput::new(&v)) + .filter(|v| items_names.contains(&v.name())) .collect::>(); let plain = self .terminal() diff --git a/implementations/rust/ockam/ockam_command/src/vault/util.rs b/implementations/rust/ockam/ockam_command/src/vault/util.rs index e9ee412fb1a..3f6a8215c8c 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/util.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/util.rs @@ -1,25 +1,26 @@ -use crate::output::Output; -use crate::OckamColor; use colorful::Colorful; use indoc::formatdoc; -use ockam_api::cli_state::{StateItemTrait, VaultConfig, VaultState}; + +use ockam_api::cli_state::vaults::NamedVault; + +use crate::output::Output; +use crate::OckamColor; #[derive(serde::Serialize)] pub struct VaultOutput { - pub(crate) name: String, - #[serde(flatten)] - config: VaultConfig, - is_default: bool, + vault: NamedVault, } impl VaultOutput { - pub fn new(state: &VaultState, is_default: bool) -> Self { + pub fn new(vault: &NamedVault) -> Self { Self { - name: state.name().to_string(), - config: state.config().clone(), - is_default, + vault: vault.clone(), } } + + pub fn name(&self) -> String { + self.vault.name().clone() + } } impl Output for VaultOutput { @@ -31,11 +32,16 @@ impl Output for VaultOutput { Type: {vault_type} "#, name = self - .name + .vault + .name() .to_string() .color(OckamColor::PrimaryResource.color()), - default = if self.is_default { "(default)" } else { "" }, - vault_type = match self.config.is_aws() { + default = if self.vault.is_default() { + "(default)" + } else { + "" + }, + vault_type = match self.vault.is_kms() { true => "AWS KMS", false => "OCKAM", } @@ -49,11 +55,16 @@ impl Output for VaultOutput { r#"Name: {name} {default} Type: {vault_type}"#, name = self - .name + .vault + .name() .to_string() .color(OckamColor::PrimaryResource.color()), - default = if self.is_default { "(default)" } else { "" }, - vault_type = match self.config.is_aws() { + default = if self.vault.is_default() { + "(default)" + } else { + "" + }, + vault_type = match self.vault.is_kms() { true => "AWS KMS", false => "OCKAM", } diff --git a/implementations/rust/ockam/ockam_command/src/worker/list.rs b/implementations/rust/ockam/ockam_command/src/worker/list.rs index 459e50db592..500f18c68d2 100644 --- a/implementations/rust/ockam/ockam_command/src/worker/list.rs +++ b/implementations/rust/ockam/ockam_command/src/worker/list.rs @@ -1,16 +1,13 @@ use clap::Args; use colorful::Colorful; -use miette::miette; use tokio::sync::Mutex; use tokio::try_join; use ockam::Context; use ockam_api::address::extract_address_value; -use ockam_api::cli_state::StateDirTrait; use ockam_api::nodes::models::workers::{WorkerList, WorkerStatus}; use ockam_api::nodes::BackgroundNode; -use crate::node::{get_node_name, initialize_node_if_default}; use crate::output::Output; use crate::terminal::OckamColor; use crate::util::{api, node_rpc}; @@ -23,19 +20,18 @@ const AFTER_LONG_HELP: &str = include_str!("./static/list/after_long_help.txt"); /// List workers on a node #[derive(Clone, Debug, Args)] #[command( - long_about = docs::about(LONG_ABOUT), - before_help = docs::before_help(PREVIEW_TAG), - after_long_help = docs::after_help(AFTER_LONG_HELP) +long_about = docs::about(LONG_ABOUT), +before_help = docs::before_help(PREVIEW_TAG), +after_long_help = docs::after_help(AFTER_LONG_HELP) )] pub struct ListCommand { /// Node at which to lookup workers - #[arg(value_name = "NODE", long, display_order = 800)] + #[arg(value_name = "NODE", long, display_order = 800, value_parser = extract_address_value)] at: Option, } impl ListCommand { pub fn run(self, opts: CommandGlobalOpts) { - initialize_node_if_default(&opts, &self.at); node_rpc(run_impl, (opts, self)) } } @@ -44,14 +40,7 @@ async fn run_impl( ctx: Context, (opts, cmd): (CommandGlobalOpts, ListCommand), ) -> miette::Result<()> { - let at = get_node_name(&opts.state, &cmd.at); - let node_name = extract_address_value(&at)?; - - if !opts.state.nodes.get(&node_name)?.is_running() { - return Err(miette!("The node '{}' is not running", node_name)); - } - - let node = BackgroundNode::create(&ctx, &opts.state, &node_name).await?; + let node = BackgroundNode::create(&ctx, &opts.state, &cmd.at).await?; let is_finished: Mutex = Mutex::new(false); let get_workers = async { @@ -62,9 +51,7 @@ async fn run_impl( let output_messages = vec![format!( "Listing Workers on {}...\n", - node_name - .to_string() - .color(OckamColor::PrimaryResource.color()) + node.node_name().color(OckamColor::PrimaryResource.color()) )]; let progress_output = opts @@ -75,8 +62,8 @@ async fn run_impl( let list = opts.terminal.build_list( &workers.list, - &format!("Workers on {node_name}"), - &format!("No workers found on {node_name}."), + &format!("Workers on {}", node.node_name()), + &format!("No workers found on {}.", node.node_name()), )?; opts.terminal.stdout().plain(list).write_line()?; diff --git a/implementations/rust/ockam/ockam_command/tests/bats/authority.bats b/implementations/rust/ockam/ockam_command/tests/bats/authority.bats index 877082d2fb1..825181398ac 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/authority.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/authority.bats @@ -13,6 +13,16 @@ teardown() { } # ===== TESTS +@test "authority - an authority node must be shown as UP even if its tcp listener cannot be accessed" { + port="$(random_port)" + + run_success "$OCKAM" identity create authority + authority_identity_full=$($OCKAM identity show --full --encoding hex authority) + trusted="{}" + run_success "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" + run_success "$OCKAM" node show authority + assert_output --partial "\"is_up\": true" +} @test "authority - standalone authority, enrollers, members" { port="$(random_port)" @@ -36,50 +46,42 @@ teardown() { # Start the authority node. We pass a set of pre trusted-identities containing m1' identity identifier # For the first test we start the node with no direct authentication service nor token enrollment trusted="{\"$m1_identifier\": {\"sample_attr\": \"sample_val\", \"project_id\" : \"1\", \"trust_context_id\" : \"1\"}, \"$enroller_identifier\": {\"project_id\": \"1\", \"trust_context_id\": \"1\", \"ockam-role\": \"enroller\"}}" - run "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" --no-direct-authentication --no-token-enrollment - assert_success + run_success "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" --no-direct-authentication --no-token-enrollment sleep 1 # wait for authority to start TCP listener - PROJECT_JSON_PATH="$OCKAM_HOME/project-authority.json" PROJECT_NAME="default" - echo "{\"id\": \"1\", - \"name\" : \"$PROJECT_NAME\", - \"identity\" : \"I6c20e814b56579306f55c64e8747e6c1b4a53d9aa1b2c3d4e5f6a6b5c4d3e2f1\", - \"access_route\" : \"/dnsaddr/127.0.0.1/tcp/4000/service/api\", - \"authority_access_route\" : \"/dnsaddr/127.0.0.1/tcp/$port/service/api\", - \"authority_identity\" : \"$authority_identity_full\"}" >"$PROJECT_JSON_PATH" + run_success bash -c "$OCKAM project import \ + --project-name $PROJECT_NAME \ + --project-id 1 \ + --project-identifier I6c20e814b56579306f55c64e8747e6c1b4a53d9aa1b2c3d4e5f6a6b5c4d3e2f1 \ + --project-access-route /dnsaddr/127.0.0.1/tcp/4000/service/api \ + --authority-identity $authority_identity_full \ + --authority-access-route /dnsaddr/127.0.0.1/tcp/$port/service/api" # m1 is a member (its on the set of pre-trusted identifiers) so it can get it's own credential - run "$OCKAM" project enroll --project-path "$PROJECT_JSON_PATH" --identity m1 - assert_success + run_success "$OCKAM" project enroll --project "$PROJECT_NAME" --identity m1 assert_output --partial "sample_val" echo "$trusted" >"$OCKAM_HOME/trusted-anchors.json" # Restart the authority node with a trusted identities file and check that m1 can still enroll - run "$OCKAM" node delete authority --yes - run "$OCKAM" authority create --tcp-listener-address=127.0.0.1:$port --project-identifier 1 --reload-from-trusted-identities-file "$OCKAM_HOME/trusted-anchors.json" - assert_success + run_success "$OCKAM" node delete authority --yes + run_success "$OCKAM" authority create --tcp-listener-address=127.0.0.1:$port --project-identifier 1 --reload-from-trusted-identities-file "$OCKAM_HOME/trusted-anchors.json" sleep 1 # wait for authority to start TCP listener - run "$OCKAM" project ticket --identity enroller --project "$PROJECT_NAME" --member $m2_identifier --attribute sample_attr=m2_member - assert_success + run_success "$OCKAM" project ticket --identity enroller --project "$PROJECT_NAME" --member $m2_identifier --attribute sample_attr=m2_member - run "$OCKAM" project enroll --force --project "$PROJECT_NAME" --identity m2 - assert_success + run_success "$OCKAM" project enroll --force --project "$PROJECT_NAME" --identity m2 assert_output --partial "m2_member" token1=$($OCKAM project ticket --identity enroller --project "$PROJECT_NAME" --attribute sample_attr=m3_member) - run "$OCKAM" project enroll --force $token1 --identity m3 - assert_success + run_success "$OCKAM" project enroll --force $token1 --identity m3 assert_output --partial "m3_member" token2=$($OCKAM project ticket --identity enroller --project "$PROJECT_NAME" --usage-count 2 --attribute sample_attr=members_group) - run "$OCKAM" project enroll --force $token2 --identity m4 - assert_success + run_success "$OCKAM" project enroll --force $token2 --identity m4 assert_output --partial "members_group" - run "$OCKAM" project enroll --force $token2 --identity m5 - assert_success + run_success "$OCKAM" project enroll --force $token2 --identity m5 assert_output --partial "members_group" run "$OCKAM" project enroll --force $token2 --identity m6 @@ -99,27 +101,26 @@ teardown() { # Start the authority node. trusted="{\"$enroller_identifier\": {\"project_id\": \"1\", \"trust_context_id\": \"1\", \"ockam-role\": \"enroller\"}}" - run "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" - assert_success + run_success "$OCKAM" authority create --tcp-listener-address="127.0.0.1:$port" --project-identifier 1 --trusted-identities "$trusted" sleep 1 # wait for authority to start TCP listener - PROJECT_JSON_PATH="$OCKAM_HOME/project-authority.json" - echo "{\"id\": \"1\", - \"name\" : \"default\", - \"identity\" : \"I6c20e814b56579306f55c64e8747e6c1b4a53d9aa1b2c3d4e5f6a6b5c4d3e2f1\", - \"access_route\" : \"/dnsaddr/127.0.0.1/tcp/4000/service/api\", - \"authority_access_route\" : \"/dnsaddr/127.0.0.1/tcp/$port/service/api\", - \"authority_identity\" : \"$authority_identity_full\"}" >"$PROJECT_JSON_PATH" + PROJECT_NAME="default" + run_success bash -c "$OCKAM project import \ + --project-name $PROJECT_NAME \ + --project-id 1 \ + --project-identifier I6c20e814b56579306f55c64e8747e6c1b4a53d9aa1b2c3d4e5f6a6b5c4d3e2f1 \ + --project-access-route /dnsaddr/127.0.0.1/tcp/4000/service/api \ + --authority-identity $authority_identity_full \ + --authority-access-route /dnsaddr/127.0.0.1/tcp/$port/service/api" # Enrollment ticket expired by the time it's used - token=$($OCKAM project ticket --identity enroller --project-path "$PROJECT_JSON_PATH" --attribute sample_attr=m3_member --expires-in 1s) + token=$($OCKAM project ticket --identity enroller --project "$PROJECT_NAME" --attribute sample_attr=m3_member --expires-in 1s) sleep 2 run "$OCKAM" project enroll $token --identity m3 assert_failure # Enrollment ticket with enough ttl - token=$($OCKAM project ticket --identity enroller --project-path "$PROJECT_JSON_PATH" --attribute sample_attr=m3_member --expires-in 30s) - run "$OCKAM" project enroll $token --identity m3 - assert_success + token=$($OCKAM project ticket --identity enroller --project "$PROJECT_NAME" --attribute sample_attr=m3_member --expires-in 30s) + run_success "$OCKAM" project enroll $token --identity m3 assert_output --partial "m3_member" } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats b/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats index 255eaa9a0bd..259fbe745b0 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats @@ -177,13 +177,11 @@ teardown() { @test "elastic encrypted relays" { setup_orchestrator_test - "$OCKAM" project information --output json >/${BATS_TEST_TMPDIR}/project.json - a="$(random_str)" b="$(random_str)" - run_success "$OCKAM" node create "$a" --project-path /${BATS_TEST_TMPDIR}/project.json - run_success "$OCKAM" node create "$b" --project-path /${BATS_TEST_TMPDIR}/project.json + run_success "$OCKAM" node create "$a" --project $PROJECT_NAME + run_success "$OCKAM" node create "$b" --project $PROJECT_NAME run_success "$OCKAM" relay create "$b" --at /project/default --to "/node/$a" output=$("$OCKAM" secure-channel create --from "$a" --to "/project/default/service/forward_to_$b/service/api" | @@ -288,13 +286,11 @@ teardown() { @test "managed authorities" { setup_orchestrator_test - "$OCKAM" project information --output json >/${BATS_TEST_TMPDIR}/project.json - a="$(random_str)" b="$(random_str)" - run_success "$OCKAM" node create "$a" --project-path /${BATS_TEST_TMPDIR}/project.json - run_success "$OCKAM" node create "$b" --project-path /${BATS_TEST_TMPDIR}/project.json + run_success "$OCKAM" node create "$a" --project $PROJECT_NAME + run_success "$OCKAM" node create "$b" --project $PROJECT_NAME run_success "$OCKAM" relay create "$b" --at /project/default --to "/node/$a/service/forward_to_$b" diff --git a/implementations/rust/ockam/ockam_command/tests/bats/load/base.bash b/implementations/rust/ockam/ockam_command/tests/bats/load/base.bash index 9f14f70407f..c8ce8d06af2 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/load/base.bash +++ b/implementations/rust/ockam/ockam_command/tests/bats/load/base.bash @@ -84,7 +84,7 @@ teardown_home_dir() { # Copy the CLI directory to $HOME/.bats-tests so it can be inspected. # For some reason, if the directory is moved, the teardown function gets stuck. echo "Failed test dir: $OCKAM_HOME" >&3 - cp -a "$OCKAM_HOME" "$HOME/.bats-tests" + cp -r "$OCKAM_HOME/." "$HOME/.bats-tests" fi run $OCKAM node delete --all --force --yes run $OCKAM reset -y diff --git a/implementations/rust/ockam/ockam_command/tests/bats/load/orchestrator.bash b/implementations/rust/ockam/ockam_command/tests/bats/load/orchestrator.bash index 38371f3341e..21168e0a828 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/load/orchestrator.bash +++ b/implementations/rust/ockam/ockam_command/tests/bats/load/orchestrator.bash @@ -16,34 +16,7 @@ function skip_if_orchestrator_tests_not_enabled() { function copy_local_orchestrator_data() { if [ ! -z "${ORCHESTRATOR_TESTS}" ]; then - cp -a $OCKAM_HOME_BASE $OCKAM_HOME - export PROJECT_JSON_PATH="$OCKAM_HOME/project.json" + cp -r $OCKAM_HOME_BASE/. $OCKAM_HOME export PROJECT_NAME="default" - cp $OCKAM_HOME/projects/default.json $PROJECT_JSON_PATH - fi -} - -function fetch_orchestrator_data() { - copy_local_orchestrator_data - max_retries=5 - i=0 - while [[ $i -lt $max_retries ]]; do - run bash -c "$OCKAM project information --output json >$PROJECT_JSON_PATH" - # if status is not 0, retry - if [ $status -ne 0 ]; then - sleep 5 - ((i++)) - continue - fi - # if file is empty, exit with error - if [ ! -s "$PROJECT_JSON_PATH" ]; then - echo "Project information is empty" >&3 - exit 1 - fi - break - done - if [ $i -eq $max_retries ]; then - echo "Failed to fetch project information" >&3 - exit 1 fi } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/nodes.bats b/implementations/rust/ockam/ockam_command/tests/bats/nodes.bats index 50e9bf06a27..0b2edaa3a22 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/nodes.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/nodes.bats @@ -15,19 +15,7 @@ teardown() { # ===== UTILS force_kill_node() { - max_retries=5 - i=0 - while [[ $i -lt $max_retries ]]; do - pid="$(cat $OCKAM_HOME/nodes/$1/pid)" - run kill -9 $pid - # Killing a node created without `-f` leaves the - # process in a defunct state when running within Docker. - if ! ps -p $pid || ps -p $pid | grep defunct; then - return - fi - sleep 0.2 - ((i = i + 1)) - done + run killall ockam } # ===== TESTS diff --git a/implementations/rust/ockam/ockam_command/tests/bats/portals_orchestrator.bats b/implementations/rust/ockam/ockam_command/tests/bats/portals_orchestrator.bats index 626da01002c..bcae8be500a 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/portals_orchestrator.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/portals_orchestrator.bats @@ -24,13 +24,13 @@ teardown() { @test "portals - create an inlet/outlet pair, a relay in an orchestrator project and move tcp traffic through it" { port="$(random_port)" - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 fwd="$(random_str)" run_success "$OCKAM" relay create "$fwd" --to /node/blue - run_success "$OCKAM" node create green --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create green --project "$PROJECT_NAME" run_success bash -c "$OCKAM secure-channel create --from /node/green --to /project/default/service/forward_to_$fwd/service/api \ | $OCKAM tcp-inlet create --at /node/green --from 127.0.0.1:$port --to -/service/outlet" @@ -38,10 +38,9 @@ teardown() { } @test "portals - create an inlet using only default arguments, an outlet, a relay in an orchestrator project and move tcp traffic through it" { - port="$(random_port)" - - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create blue run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 + run_success "$OCKAM" relay create --to /node/blue addr=$($OCKAM tcp-inlet create) @@ -51,13 +50,13 @@ teardown() { @test "portals - create an inlet (with implicit secure channel creation), an outlet, a relay in an orchestrator project and move tcp traffic through it" { port="$(random_port)" - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 fwd="$(random_str)" run_success "$OCKAM" relay create "$fwd" --to /node/blue - run_success "$OCKAM" node create green --project "$PROJECT_JSON_PATH" + run_success "$OCKAM" node create green --project "$PROJECT_NAME" run_success "$OCKAM" tcp-inlet create --at /node/green --from "127.0.0.1:$port" --to "/project/default/service/forward_to_$fwd/secure/api/service/outlet" run_success curl --fail --head --max-time 10 "127.0.0.1:$port" @@ -70,14 +69,15 @@ teardown() { # Setup nodes from a non-enrolled environment setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green run_success "$OCKAM" identity create blue green_identifier=$($OCKAM identity show green) blue_identifier=$($OCKAM identity show blue) - run_success "$OCKAM" node create green --project-path "$PROJECT_JSON_PATH" --identity green - run_success "$OCKAM" node create blue --project-path "$PROJECT_JSON_PATH" --identity blue + run_success "$OCKAM" node create green --project "$PROJECT_NAME" --identity green + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --identity blue # Green isn't enrolled as project member OCKAM_HOME=$ENROLLED_OCKAM_HOME @@ -90,7 +90,7 @@ teardown() { run_success "$OCKAM" relay create "$fwd" --to /node/blue assert_output --partial "forward_to_$fwd" - run_success bash -c "$OCKAM secure-channel create --from /node/green --to /project/default/service/forward_to_$fwd/service/api \ + run_success bash -c "$OCKAM secure-channel create --from /node/green --identity green --to /project/default/service/forward_to_$fwd/service/api \ | $OCKAM tcp-inlet create --at /node/green --from 127.0.0.1:$port --to -/service/outlet" # Green can't establish secure channel with blue, because it didn't exchange credential with it. @@ -102,14 +102,15 @@ teardown() { ENROLLED_OCKAM_HOME=$OCKAM_HOME setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green run_success "$OCKAM" identity create blue green_identifier=$($OCKAM identity show green) blue_identifier=$($OCKAM identity show blue) - run_success "$OCKAM" node create green --project-path "$PROJECT_JSON_PATH" --identity green - run_success "$OCKAM" node create blue --project-path "$PROJECT_JSON_PATH" --identity blue + run_success "$OCKAM" node create green --project "$PROJECT_NAME" --identity green + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --identity blue # Green isn't enrolled as project member OCKAM_HOME=$ENROLLED_OCKAM_HOME @@ -131,14 +132,15 @@ teardown() { ENROLLED_OCKAM_HOME=$OCKAM_HOME setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green run_success "$OCKAM" identity create blue green_identifier=$($OCKAM identity show green) blue_identifier=$($OCKAM identity show blue) - run_success "$OCKAM" node create green --project-path "$PROJECT_JSON_PATH" --identity green - run_success "$OCKAM" node create blue --project-path "$PROJECT_JSON_PATH" --identity blue + run_success "$OCKAM" node create green --project "$PROJECT_NAME" --identity green + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --identity blue OCKAM_HOME=$ENROLLED_OCKAM_HOME run_success "$OCKAM" project ticket --member "$blue_identifier" --attribute role=member @@ -166,16 +168,17 @@ teardown() { setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green run_success "$OCKAM" identity create blue run_success "$OCKAM" project enroll $green_token --identity green - run_success "$OCKAM" node create green --project-path "$PROJECT_JSON_PATH" --identity green + run_success "$OCKAM" node create green --project "$PROJECT_NAME" --identity green run_success "$OCKAM" policy create --at green --resource tcp-inlet --expression '(= subject.app "app1")' run_success "$OCKAM" project enroll $blue_token --identity blue - run_success "$OCKAM" node create blue --project-path "$PROJECT_JSON_PATH" --identity blue + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --identity blue run_success "$OCKAM" policy create --at blue --resource tcp-outlet --expression '(= subject.app "app1")' run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 @@ -209,21 +212,22 @@ teardown() { run_success curl --head --retry-connrefused --retry 20 --retry-max-time 20 --max-time 1 "127.0.0.1:$port" } -@test "portals - local inlet and outlet passing trhough a relay, removing and re-creating the outlet" { +@test "portals - local inlet and outlet passing through a relay, removing and re-creating the outlet" { port="$(random_port)" node_port="$(random_port)" - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" --tcp-listener-address "127.0.0.1:$node_port" + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --tcp-listener-address "127.0.0.1:$node_port" run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 + run_success "$OCKAM" relay create --to /node/blue - run_success "$OCKAM" node create green --project "$PROJECT_JSON_PATH" - run_success "$OCKAM" tcp-inlet create --at /node/green --from "127.0.0.1:$port" --to /project/default/service/forward_to_default/secure/api/service/outlet + run_success "$OCKAM" node create green --project "$PROJECT_NAME" + run_success "$OCKAM" tcp-inlet create --at /node/green --from "127.0.0.1:$port" --to "/project/default/service/forward_to_default/secure/api/service/outlet" run_success curl --fail --head --max-time 10 "127.0.0.1:$port" $OCKAM node delete blue --yes run_failure curl --fail --head --max-time 2 "127.0.0.1:$port" - run_success "$OCKAM" node create blue --project "$PROJECT_JSON_PATH" --tcp-listener-address "127.0.0.1:$node_port" + run_success "$OCKAM" node create blue --project "$PROJECT_NAME" --tcp-listener-address "127.0.0.1:$node_port" run_success "$OCKAM" relay create --to /node/blue run_success "$OCKAM" tcp-outlet create --at /node/blue --to 127.0.0.1:5000 run_success curl --head --retry-connrefused --retry 50 --max-time 1 "127.0.0.1:$port" diff --git a/implementations/rust/ockam/ockam_command/tests/bats/projects.bats b/implementations/rust/ockam/ockam_command/tests/bats/projects.bats new file mode 100644 index 00000000000..d6f36a59c46 --- /dev/null +++ b/implementations/rust/ockam/ockam_command/tests/bats/projects.bats @@ -0,0 +1,27 @@ +#!/bin/bash + +# ===== SETUP + +setup() { + load load/base.bash + load_bats_ext + setup_home_dir +} + +teardown() { + teardown_home_dir +} + +# ===== TESTS + +@test "projects - a project can be imported" { + run_success bash -c "$OCKAM project import \ + --project-name awesome \ + --project-id 1 \ + --project-identifier Ie92f183eb4c324804ef4d62962dea94cf095a265a1b2c3d4e5f6a6b5c4d3e2f1 \ + --project-access-route /dnsaddr/127.0.0.1/tcp/4000/service/api \ + --authority-identity 81825837830101583285f68200815820f02eb8b3f7b97e73f4866cc76953e0fe8aa8765b69bac1bc630b3756c587aa9bf41a6565bf201a7831c2208200815840f2474f917cac6a315a780034ec54786be9368ea0e50b713eb4847571efca8f98ece6f470ef7d18deefc134752db175e7f40e154b1a7c002d9b29db0c65892a08 \ + --authority-access-route /dnsaddr/127.0.0.1/tcp/5000/service/api" + + assert_output --partial "Successfully imported project awesome" +} diff --git a/implementations/rust/ockam/ockam_command/tests/bats/projects_orchestrator.bats b/implementations/rust/ockam/ockam_command/tests/bats/projects_orchestrator.bats index 07556e2df33..b7ce84b96a9 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/projects_orchestrator.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/projects_orchestrator.bats @@ -39,8 +39,10 @@ teardown() { @test "projects - enrollment" { ENROLLED_OCKAM_HOME=$OCKAM_HOME + # Change to a new home directory where there are no enrolled identities setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME run_success "$OCKAM" identity create green green_identifier=$($OCKAM identity show green) @@ -49,7 +51,7 @@ teardown() { blue_identifier=$($OCKAM identity show blue) # They haven't been added by enroller yet - run_failure "$OCKAM" project enroll --identity green --project-path "$PROJECT_JSON_PATH" + run_failure "$OCKAM" project enroll --identity green OCKAM_HOME=$ENROLLED_OCKAM_HOME $OCKAM project ticket --member "$green_identifier" --attribute role=member @@ -57,7 +59,7 @@ teardown() { OCKAM_HOME=$NON_ENROLLED_OCKAM_HOME # Green' identity was added by enroller - run_success "$OCKAM" project enroll --identity green --project-path "$PROJECT_JSON_PATH" + run_success "$OCKAM" project enroll --identity green assert_output --partial "$green_identifier" # For blue, we use an enrollment token generated by enroller @@ -72,17 +74,18 @@ teardown() { # Change to a new home directory where there are no enrolled identities setup_home_dir NON_ENROLLED_OCKAM_HOME=$OCKAM_HOME + cp -r $ENROLLED_OCKAM_HOME/. $NON_ENROLLED_OCKAM_HOME # Create a named default identity run_success "$OCKAM" identity create green green_identifier=$($OCKAM identity show green) # Create node for the non-enrolled identity using the exported project information - run_success "$OCKAM" node create green --project-path "$ENROLLED_OCKAM_HOME/project.json" + run_success "$OCKAM" node create green --project $PROJECT_NAME # Node can't create relay as it isn't a member fwd=$(random_str) - run_failure "$OCKAM" relay create "$fwd" + run_failure "$OCKAM" relay create --identity green "$fwd" # Add node as a member OCKAM_HOME=$ENROLLED_OCKAM_HOME @@ -99,9 +102,9 @@ teardown() { # than the admin?. If we pass project' address directly (instead of /project/ thing), would # it present credential? would read authority info from project.json? - run_success "$OCKAM" project information --output json >/tmp/project.json - + cp -r $OCKAM_HOME/. /tmp/ockam export OCKAM_HOME=/tmp/ockam + run_success "$OCKAM" identity create m2 run_success "$OCKAM" identity create m1 m1_identifier=$($OCKAM identity show m1) @@ -111,14 +114,14 @@ teardown() { export OCKAM_HOME=/tmp/ockam # m1' identity was added by enroller - run_success $OCKAM project enroll --identity m1 --project-path "$PROJECT_JSON_PATH" + run_success $OCKAM project enroll --identity m1 --project $PROJECT_NAME # m1 is a member, must be able to contact the project' service - run_success $OCKAM message send --timeout 5 --identity m1 --project-path "$PROJECT_JSON_PATH" --to /project/default/service/echo hello + run_success $OCKAM message send --timeout 5 --identity m1 --project $PROJECT_NAME --to /project/default/service/echo hello assert_output "hello" # m2 is not a member, must not be able to contact the project' service - run_failure $OCKAM message send --timeout 5 --identity m2 --project-path "$PROJECT_JSON_PATH" --to /project/default/service/echo hello + run_failure $OCKAM message send --timeout 5 --identity m2 --project $PROJECT_NAME --to /project/default/service/echo hello } @test "projects - list addons" { @@ -158,9 +161,9 @@ teardown() { sleep 30 #FIXME workaround, project not yet ready after configuring addon - run_success "$OCKAM" project information default --output json >/tmp/project.json - + cp -r $OCKAM_HOME/. /tmp/ockam export OCKAM_HOME=/tmp/ockam + run_success "$OCKAM" identity create m1 run_success "$OCKAM" identity create m2 run_success "$OCKAM" identity create m3 @@ -172,21 +175,22 @@ teardown() { run_success "$OCKAM" project ticket --member $m1_identifier --attribute service=sensor run_success "$OCKAM" project ticket --member $m2_identifier --attribute service=web + cp -r $OCKAM_HOME/. /tmp/ockam export OCKAM_HOME=/tmp/ockam # m1 and m2 identity was added by enroller - run_success "$OCKAM" project enroll --identity m1 --project-path "$PROJECT_JSON_PATH" + run_success "$OCKAM" project enroll --identity m1 --project $PROJECT_NAME assert_output --partial $green_identifier - run_success "$OCKAM" project enroll --identity m2 --project-path "$PROJECT_JSON_PATH" + run_success "$OCKAM" project enroll --identity m2 --project $PROJECT_NAME assert_output --partial $green_identifier # m1 and m2 can use the lease manager - run_success "$OCKAM" lease --identity m1 --project-path "$PROJECT_JSON_PATH" create - run_success "$OCKAM" lease --identity m2 --project-path "$PROJECT_JSON_PATH" create + run_success "$OCKAM" lease --identity m1 --project $PROJECT_NAME create + run_success "$OCKAM" lease --identity m2 --project $PROJECT_NAME create # m3 can't - run_success "$OCKAM" lease --identity m3 --project-path "$PROJECT_JSON_PATH" create + run_success "$OCKAM" lease --identity m3 --project $PROJECT_NAME create assert_failure unset OCKAM_HOME @@ -194,10 +198,12 @@ teardown() { sleep 30 #FIXME workaround, project not yet ready after configuring addon + cp -r $OCKAM_HOME/. /tmp/ockam export OCKAM_HOME=/tmp/ockam + # m1 can use the lease manager (it has a service=sensor attribute attested by authority) - run_success "$OCKAM" lease --identity m1 --project-path "$PROJECT_JSON_PATH" create + run_success "$OCKAM" lease --identity m1 --project $PROJECT_NAME create # m2 can't use the lease manager now (it doesn't have a service=sensor attribute attested by authority) - run_failure "$OCKAM" lease --identity m2 --project-path "$PROJECT_JSON_PATH" create + run_failure "$OCKAM" lease --identity m2 --project $PROJECT_NAME create } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/trust_context.bats b/implementations/rust/ockam/ockam_command/tests/bats/trust_context.bats index 5bd5848b7c1..3c6e31eb74f 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/trust_context.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/trust_context.bats @@ -54,15 +54,12 @@ teardown() { @test "trust context - trust context with an id only; ABAC rules are applied" { run_success "$OCKAM" identity create m1 - echo "{ - \"id\": \"1\" - }" >"$OCKAM_HOME/trust_context.json" - m1_identifier=$(run_success "$OCKAM" identity show m1) trusted="{\"$m1_identifier\": {\"sample_attr\": \"sample_val\", \"project_id\" : \"1\", \"trust_context_id\" : \"1\"}}" run_success "$OCKAM" node create n1 --identity m1 - run_success "$OCKAM" node create n2 --trust-context "$OCKAM_HOME/trust_context.json" --trusted-identities "$trusted" + run_success "$OCKAM" trust-context create default --id 1 + run_success "$OCKAM" node create n2 --trust-context default --trusted-identities "$trusted" run_success bash -c "$OCKAM secure-channel create --from /node/n1 --to /node/n2/service/api \ | $OCKAM message send hello --from /node/n1 --to -/service/echo" run_failure "$OCKAM" message send hello --timeout 2 --from /node/n1 --to /node/n2/service/echo @@ -113,8 +110,9 @@ teardown() { assert_output $msg run_success "$OCKAM" node delete alice --yes - echo "{\"id\": \"$authority_id\"}" >"$OCKAM_HOME/alice-trust-context.json" - run_success "$OCKAM" node create alice --tcp-listener-address 127.0.0.1:$port --identity alice --trust-context "$OCKAM_HOME/alice-trust-context.json" + run_success "$OCKAM" trust-context create alice-trust-context --id "$authority_id" + + run_success "$OCKAM" node create alice --tcp-listener-address 127.0.0.1:$port --identity alice --trust-context alice-trust-context run_failure "$OCKAM" message send --timeout 2 --identity bob --to /dnsaddr/127.0.0.1/tcp/$port/secure/api/service/echo --trust-context bob-trust-context $msg } @@ -135,26 +133,20 @@ teardown() { assert_success sleep 1 - echo "{\"id\": \"test-context\", - \"authority\" : { - \"identity\" : \"$authority_identity\", - \"own_credential\" :{ - \"FromCredentialIssuer\" : { - \"identity\": \"$authority_identity\", - \"multiaddr\" : \"/dnsaddr/127.0.0.1/tcp/$auth_port/service/api\" }}}}" >"$OCKAM_HOME/trust_context.json" - - run_success "$OCKAM" node create --identity alice --tcp-listener-address 127.0.0.1:$node_port --trust-context "$OCKAM_HOME/trust_context.json" + authority_route="/dnsaddr/127.0.0.1/tcp/$auth_port/service/api" + run_success "$OCKAM" trust-context create test-context --id test-context --authority-identity $authority_identity --authority-route $authority_route + run_success "$OCKAM" node create --identity alice --tcp-listener-address 127.0.0.1:$node_port --trust-context test-context sleep 1 # send a message to alice using the trust context msg=$(random_str) - run_success "$OCKAM" message send --identity bob --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context "$OCKAM_HOME/trust_context.json" $msg + run_success "$OCKAM" message send --timeout 2 --identity bob --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context test-context $msg assert_output "$msg" # send a message to authority node echo service to make sure we can use it as a healthcheck endpoint run_success "$OCKAM" message send --timeout 2 --identity bob --to "/dnsaddr/127.0.0.1/tcp/$auth_port/secure/api/service/echo" $msg assert_output "$msg" - run_failure "$OCKAM" message send --timeout 2 --identity attacker --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context "$OCKAM_HOME/trust_context.json" $msg + run_failure "$OCKAM" message send --timeout 2 --identity attacker --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context test-context $msg run_failure "$OCKAM" message send --timeout 2 --identity attacker --to /dnsaddr/127.0.0.1/tcp/$node_port/secure/api/service/echo --trust-context $msg } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/use_cases.bats b/implementations/rust/ockam/ockam_command/tests/bats/use_cases.bats index 575d31120f3..2959a67b5cd 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/use_cases.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/use_cases.bats @@ -67,40 +67,46 @@ teardown() { @test "use-case - abac" { skip_if_orchestrator_tests_not_enabled copy_local_orchestrator_data + export OCKAM_BASE_HOME=$OCKAM_HOME port_1=9002 port_2=9003 # Administrator - ADMIN_OCKAM_HOME=$OCKAM_HOME + setup_home_dir + cp -r $OCKAM_BASE_HOME/. $OCKAM_HOME + cp1_token=$($OCKAM project ticket --attribute component=control) ep1_token=$($OCKAM project ticket --attribute component=edge) x_token=$($OCKAM project ticket --attribute component=x) # Control plane setup_home_dir - CONTROL_OCKAM_HOME=$OCKAM_HOME + cp -r $OCKAM_BASE_HOME/. $OCKAM_HOME + fwd=$(random_str) $OCKAM identity create control_identity - $OCKAM project enroll $cp1_token --project-path "$PROJECT_JSON_PATH" --identity control_identity - $OCKAM node create control_plane1 --project-path "$PROJECT_JSON_PATH" --identity control_identity + $OCKAM project enroll $cp1_token --project "$PROJECT_NAME" --identity control_identity + $OCKAM node create control_plane1 --project "$PROJECT_NAME" --identity control_identity $OCKAM policy create --at control_plane1 --resource tcp-outlet --expression '(= subject.component "edge")' $OCKAM tcp-outlet create --at /node/control_plane1 --to 127.0.0.1:5000 run_success "$OCKAM" relay create "$fwd" --to /node/control_plane1 # Edge plane setup_home_dir + cp -r $OCKAM_BASE_HOME/. $OCKAM_HOME + $OCKAM identity create edge_identity - $OCKAM project enroll $ep1_token --project-path "$PROJECT_JSON_PATH" --identity edge_identity - $OCKAM node create edge_plane1 --project-path "$PROJECT_JSON_PATH" --identity edge_identity + $OCKAM project enroll $ep1_token --project "$PROJECT_NAME" --identity edge_identity + $OCKAM node create edge_plane1 --project "$PROJECT_NAME" --identity edge_identity $OCKAM policy create --at edge_plane1 --resource tcp-inlet --expression '(= subject.component "control")' $OCKAM tcp-inlet create --at /node/edge_plane1 --from "127.0.0.1:$port_1" --to "/project/default/service/forward_to_$fwd/secure/api/service/outlet" run_success curl --fail --head --max-time 5 "127.0.0.1:$port_1" ## The following is denied $OCKAM identity create x_identity - $OCKAM project enroll $x_token --project-path "$PROJECT_JSON_PATH" --identity x_identity - $OCKAM node create x --project-path "$PROJECT_JSON_PATH" --identity x_identity + $OCKAM project enroll $x_token --project "$PROJECT_NAME" --identity x_identity + $OCKAM node create x --project "$PROJECT_NAME" --identity x_identity $OCKAM policy create --at x --resource tcp-inlet --expression '(= subject.component "control")' $OCKAM tcp-inlet create --at /node/x --from "127.0.0.1:$port_2" --to "/project/default/service/forward_to_$fwd/secure/api/service/outlet" run curl --fail --head --max-time 5 "127.0.0.1:$port_2" diff --git a/implementations/rust/ockam/ockam_command/tests/bats/vault.bats b/implementations/rust/ockam/ockam_command/tests/bats/vault.bats index a44dc8c69d4..657ca071740 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/vault.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/vault.bats @@ -20,7 +20,7 @@ teardown() { run_success "$OCKAM" vault show "${v1}" --output json assert_output --partial "\"name\":\"${v1}\"" - assert_output --partial "\"aws_kms\":false" + assert_output --partial "\"is_kms\":false" run_success "$OCKAM" vault list --output json assert_output --partial "\"is_default\":true" @@ -30,12 +30,12 @@ teardown() { run_success "$OCKAM" vault show "${v2}" --output json assert_output --partial "\"name\":\"${v2}\"" - assert_output --partial "\"aws_kms\":false" + assert_output --partial "\"is_kms\":false" run_success "$OCKAM" vault list --output json assert_output --partial "\"name\":\"${v1}\"" assert_output --partial "\"name\":\"${v2}\"" - assert_output --partial "\"aws_kms\":false" + assert_output --partial "\"is_kms\":false" assert_output --partial "\"is_default\":true" assert_output --partial "\"is_default\":false" } diff --git a/implementations/rust/ockam/ockam_command/tests/fixtures/user.enrollment.ticket b/implementations/rust/ockam/ockam_command/tests/fixtures/user.enrollment.ticket index 71e376083ab..c42ce8be06d 100644 --- a/implementations/rust/ockam/ockam_command/tests/fixtures/user.enrollment.ticket +++ b/implementations/rust/ockam/ockam_command/tests/fixtures/user.enrollment.ticket @@ -1 +1 @@ -7b226f6e655f74696d655f636f6465223a2263306633656163373862383238633665346564316263393935626261626431653432373436653332343137373332373564303230313165326137326263623133222c2270726f6a656374223a7b226e6f64655f726f757465223a222f646e73616464722f6b38732d6875626465762d6e67696e78696e672d383536373330343661622d363630656539343138303064346131312e656c622e75732d776573742d312e616d617a6f6e6177732e636f6d2f7463702f343031312f736572766963652f617069222c226964223a2232646163363536342d656537382d346261622d623938322d643432306238646261643966222c226e616d65223a2264656661756c74222c226964656e746974795f6964223a224939623061323235633062643763386435313037376535303737386432623565373163343238366265323336323963376430636365313562316361366330393163222c22617574686f72697479223a7b226964223a224963633135613131346433636438666532643234383238336138363137643430306261376237653162633636396230366638323433346532633932323865616464222c2261646472657373223a222f646e73616464722f6b38732d6875626465762d6e67696e78696e672d383536373330343661622d363630656539343138303064346131312e656c622e75732d776573742d312e616d617a6f6e6177732e636f6d2f7463702f343031322f736572766963652f617069222c226964656e74697479223a2238313832353833373833303130313538333238356636383230303831353832303831333565613438623033306639653633656164303164363437363664353165646330663030663133396266633864336562313132393032656537386165616566343161363534643031346231613738313930343462383230303831353834303637373536336363646530303561373736363535313961336136316263323939313033336138656163353264303461333162663261626639633062306165303530346163313535303431306563383037636336633965623161663032346230643666326564643238323933393334653036336630663731653830366334393066227d2c226f6b7461223a6e756c6c7d2c2274727573745f636f6e74657874223a6e756c6c7d +7b226f6e655f74696d655f636f6465223a2230306438663166323239356461316165303437393366653430343730663131623362663639386164613861656664623336393935383362303336663230393430222c2270726f6a656374223a7b226964223a2270726f6a6563745f6964222c226e616d65223a2270726f6a6563745f6e616d65222c2273706163655f6e616d65223a2273706163655f6e616d65222c226163636573735f726f757465223a22726f757465222c227573657273223a5b5d2c2273706163655f6964223a2273706163655f6964222c226964656e74697479223a6e756c6c2c22617574686f726974795f6163636573735f726f757465223a222f70726f6a6563742f617574686f726974795f726f757465222c22617574686f726974795f6964656e74697479223a2238316132303135383362613230313031303235383335613430323832303138313538323061666263613963663564343430313437343530663966306430613033386133333762336665356331373038363136336632633534353039353538623632656634303366343034316136346464343034613035316137376139343334613032383230313831353834303737353432313435343563646136653766663439313336663637633963373937336563333039636134303837333630613966383434616163393631663861666533663537396137326330633935333066336666323130663032623763356635366539366365313265653235366230316437363238353139383030373233383035222c2276657273696f6e223a6e756c6c2c2272756e6e696e67223a6e756c6c2c226f7065726174696f6e5f6964223a6e756c6c2c22757365725f726f6c6573223a5b5d7d7d diff --git a/implementations/rust/ockam/ockam_identity/Cargo.toml b/implementations/rust/ockam/ockam_identity/Cargo.toml index 90ef7d3b60f..ef299d5c6e4 100644 --- a/implementations/rust/ockam/ockam_identity/Cargo.toml +++ b/implementations/rust/ockam/ockam_identity/Cargo.toml @@ -36,6 +36,7 @@ OCKAM_XX_25519_ChaChaPolyBLAKE2s = [ # be available on a standard platform. std = [ "alloc", + "chrono/std", "ockam_core/std", "ockam_macros/std", "ockam_node/std", @@ -44,11 +45,9 @@ std = [ "serde_bare/std", "minicbor/std", "time/std", - "lmdb", + "storage", ] -lmdb = ["tokio-retry", "lmdb-rkv"] - debugger = ["ockam_core/debugger"] # Feature: "no_std" enables functionality required for platforms @@ -70,29 +69,28 @@ alloc = [ "serde_bare/alloc", ] -# Feature: "sqlite" enables functionality to use sqlite for identity and policy storage -sqlite = ["rusqlite"] +storage = ["ockam_vault/storage", "sqlx", "tokio-retry"] [dependencies] async-trait = "0.1.74" cfg-if = "1.0.0" +chrono = { version = "0.4.31", default-features = false } delegate = "0.10.0" group = { version = "0.13.0", default-features = false } heapless = "0.7" hex = { version = "0.4", default-features = false } -lmdb-rkv = { version = "0.14.0", optional = true } minicbor = { version = "0.20.0", features = ["alloc", "derive"] } ockam_core = { path = "../ockam_core", version = "^0.93.0", default-features = false } ockam_macros = { path = "../ockam_macros", version = "^0.32.0", default-features = false } ockam_node = { path = "../ockam_node", version = "^0.98.0", default-features = false } ockam_vault = { path = "../ockam_vault", version = "^0.91.0", default-features = false, optional = true } rand = { version = "0.8", default-features = false } -rusqlite = { version = "0.30.0", optional = true } serde = { version = "1.0", default-features = false, features = ["derive"] } serde-big-array = "0.5" serde_bare = { version = "0.5.0", default-features = false, features = ["alloc"] } serde_json = { version = "1.0", optional = true } sha2 = { version = "0.10", default-features = false } +sqlx = { version = "0.7.3", optional = true } subtle = { version = "2.4.1", default-features = false } time = { version = "0.3.30", features = ["macros", "formatting", "std"], optional = true } tokio-retry = { version = "0.3.0", default-features = false, optional = true } diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/authority_service.rs b/implementations/rust/ockam/ockam_identity/src/credentials/authority_service.rs index 663af865637..babb516c4b5 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/authority_service.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/authority_service.rs @@ -65,6 +65,7 @@ impl AuthorityService { let credential = retriever.retrieve(ctx, subject).await?; debug!("retrieved a credential for subject {}", subject); + debug!("verifying the credential for authority {}", self.identifier); let credential_data = self .credentials .credentials_verification() diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials.rs index d5b30050e02..d4c6cba29ca 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials.rs @@ -1,9 +1,12 @@ -use crate::models::{CredentialData, PurposeKeyAttestationData}; -use crate::{CredentialsCreation, CredentialsVerification, IdentitiesRepository, PurposeKeys}; - use ockam_core::compat::sync::Arc; use ockam_vault::{VaultForSigning, VaultForVerifyingSignatures}; +use crate::models::{CredentialData, PurposeKeyAttestationData}; +use crate::{ + CredentialsCreation, CredentialsVerification, IdentitiesCreation, IdentityAttributesRepository, + PurposeKeys, +}; + /// Structure with both [`CredentialData`] and [`PurposeKeyAttestationData`] that we get /// after parsing and verifying corresponding [`Credential`] and [`super::super::models::PurposeKeyAttestation`] #[derive(Clone, Debug, PartialEq, Eq)] @@ -19,7 +22,8 @@ pub struct Credentials { credential_vault: Arc, verifying_vault: Arc, purpose_keys: Arc, - identities_repository: Arc, + identities_creation: Arc, + identity_attributes_repository: Arc, } impl Credentials { @@ -28,13 +32,15 @@ impl Credentials { credential_vault: Arc, verifying_vault: Arc, purpose_keys: Arc, - identities_repository: Arc, + identities_creation: Arc, + identity_attributes_repository: Arc, ) -> Self { Self { credential_vault, verifying_vault, purpose_keys, - identities_repository, + identities_creation, + identity_attributes_repository, } } @@ -43,18 +49,13 @@ impl Credentials { self.purpose_keys.clone() } - /// [`IdentitiesRepository`] - pub fn identities_repository(&self) -> Arc { - self.identities_repository.clone() - } - /// Return [`CredentialsCreation`] pub fn credentials_creation(&self) -> Arc { Arc::new(CredentialsCreation::new( self.purpose_keys.purpose_keys_creation(), self.credential_vault.clone(), self.verifying_vault.clone(), - self.identities_repository.clone(), + self.identities_creation.clone(), )) } @@ -63,24 +64,27 @@ impl Credentials { Arc::new(CredentialsVerification::new( self.purpose_keys.purpose_keys_verification(), self.verifying_vault.clone(), - self.identities_repository.clone(), + self.identity_attributes_repository.clone(), )) } } #[cfg(test)] mod tests { - use crate::identities::identities; - use crate::models::CredentialSchemaIdentifier; - use crate::Attributes; + use std::time::Duration; + use minicbor::bytes::ByteVec; + use ockam_core::compat::collections::BTreeMap; use ockam_core::Result; - use std::time::Duration; + + use crate::identities::identities; + use crate::models::CredentialSchemaIdentifier; + use crate::Attributes; #[tokio::test] async fn test_issue_credential() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let creation = identities.identities_creation(); let issuer = creation.create_identity().await?; diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_creation.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_creation.rs index 73676e57578..bb592abc320 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_creation.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_creation.rs @@ -1,18 +1,19 @@ -use crate::models::{Attributes, Credential, CredentialAndPurposeKey, CredentialData, Identifier}; -use crate::utils::{add_seconds, now}; -use crate::{IdentitiesRepository, Identity, PurposeKeyCreation}; - use core::time::Duration; + use ockam_core::compat::sync::Arc; use ockam_core::Result; use ockam_vault::{VaultForSigning, VaultForVerifyingSignatures}; +use crate::models::{Attributes, Credential, CredentialAndPurposeKey, CredentialData, Identifier}; +use crate::utils::{add_seconds, now}; +use crate::{IdentitiesCreation, PurposeKeyCreation}; + /// Service for managing [`Credential`]s pub struct CredentialsCreation { purpose_keys_creation: Arc, credential_vault: Arc, verifying_vault: Arc, - identities_repository: Arc, + identities_creation: Arc, } impl CredentialsCreation { @@ -21,20 +22,15 @@ impl CredentialsCreation { purpose_keys_creation: Arc, credential_vault: Arc, verifying_vault: Arc, - identities_repository: Arc, + identities_creation: Arc, ) -> Self { Self { purpose_keys_creation, verifying_vault, credential_vault, - identities_repository, + identities_creation, } } - - /// [`IdentitiesRepository`] - pub fn identities_repository(&self) -> Arc { - self.identities_repository.clone() - } } impl CredentialsCreation { @@ -52,13 +48,7 @@ impl CredentialsCreation { .get_or_create_credential_purpose_key(issuer) .await?; - let subject_change_history = self.identities_repository.get_identity(subject).await?; - let subject_identity = Identity::import_from_change_history( - Some(subject), - subject_change_history, - self.verifying_vault.clone(), - ) - .await?; + let subject_identity = self.identities_creation.get_identity(subject).await?; let created_at = now()?; let expires_at = add_seconds(&created_at, ttl.as_secs()); diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_issuer.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_issuer.rs index 22cb65dab9c..d3ff24e3e64 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_issuer.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_issuer.rs @@ -1,6 +1,7 @@ -use crate::models::{Attributes, CredentialAndPurposeKey, CredentialSchemaIdentifier, Identifier}; -use crate::utils::AttributesBuilder; -use crate::{Credentials, IdentitiesRepository, IdentitySecureChannelLocalInfo}; +use core::time::Duration; + +use minicbor::Decoder; +use tracing::trace; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::boxed::Box; @@ -11,9 +12,9 @@ use ockam_core::compat::vec::Vec; use ockam_core::{Result, Routed, Worker}; use ockam_node::Context; -use core::time::Duration; -use minicbor::Decoder; -use tracing::trace; +use crate::models::{Attributes, CredentialAndPurposeKey, CredentialSchemaIdentifier, Identifier}; +use crate::utils::AttributesBuilder; +use crate::{Credentials, IdentityAttributesRepository, IdentitySecureChannelLocalInfo}; /// Name of the attribute identifying the trust context for that attribute, meaning /// from which set of trusted authorities the attribute comes from @@ -30,7 +31,7 @@ pub const MAX_CREDENTIAL_VALIDITY: Duration = Duration::from_secs(30 * 24 * 3600 /// This struct runs as a Worker to issue credentials based on a request/response protocol pub struct CredentialsIssuer { - identities_repository: Arc, + identity_attributes_repository: Arc, credentials: Arc, issuer: Identifier, subject_attributes: Attributes, @@ -39,7 +40,7 @@ pub struct CredentialsIssuer { impl CredentialsIssuer { /// Create a new credentials issuer pub fn new( - identities_repository: Arc, + identity_attributes_repository: Arc, credentials: Arc, issuer: &Identifier, trust_context: String, @@ -49,7 +50,7 @@ impl CredentialsIssuer { .build(); Self { - identities_repository, + identity_attributes_repository, credentials, issuer: issuer.clone(), subject_attributes, @@ -61,8 +62,7 @@ impl CredentialsIssuer { subject: &Identifier, ) -> Result> { let entry = match self - .identities_repository - .as_attributes_reader() + .identity_attributes_repository .get_attributes(subject) .await? { diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_server_worker.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_server_worker.rs index 78a2d796754..ac3b81ad1d1 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_server_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_server_worker.rs @@ -78,7 +78,7 @@ impl CredentialsServerWorker { .credentials_verification() .receive_presented_credential( &sender, - self.trust_context.authorities().await?.as_slice(), + &self.trust_context.authorities(), &credential_and_purpose_key, ) .await; @@ -110,7 +110,7 @@ impl CredentialsServerWorker { .credentials_verification() .receive_presented_credential( &sender, - self.trust_context.authorities().await?.as_slice(), + &self.trust_context.authorities(), &credential_and_purpose_key, ) .await; @@ -128,13 +128,12 @@ impl CredentialsServerWorker { ); let credential = self .trust_context - .authority()? - .credential(ctx, &self.identifier) - .await; + .get_credential(ctx, &self.identifier) + .await?; match credential.as_ref() { - Ok(p) if self.present_back => { + Some(c) if self.present_back => { info!("Mutual credential presentation request processed successfully with {}. Responding with own credential...", sender); - Response::ok(req).body(p).to_vec()? + Response::ok(req).body(c).to_vec()? } _ => { info!("Mutual credential presentation request processed successfully with {}. No credential to respond!", sender); diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_verification.rs b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_verification.rs index 828c4673454..1ada57275f2 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/credentials_verification.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/credentials_verification.rs @@ -1,19 +1,21 @@ +use tracing::{debug, warn}; + +use ockam_core::compat::collections::BTreeMap; +use ockam_core::compat::sync::Arc; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; +use ockam_vault::VaultForVerifyingSignatures; + use crate::identities::AttributesEntry; use crate::models::{ CredentialAndPurposeKey, CredentialData, Identifier, PurposePublicKey, VersionedData, }; use crate::utils::now; use crate::{ - CredentialAndPurposeKeyData, IdentitiesRepository, IdentityError, PurposeKeyVerification, - TimestampInSeconds, + CredentialAndPurposeKeyData, IdentityAttributesRepository, IdentityError, + PurposeKeyVerification, TimestampInSeconds, }; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::sync::Arc; -use ockam_core::compat::vec::Vec; -use ockam_core::Result; -use ockam_vault::VaultForVerifyingSignatures; - /// We allow Credentials to be created in the future related to this machine's time due to /// possible time dyssynchronization const MAX_ALLOWED_TIME_DRIFT: TimestampInSeconds = TimestampInSeconds(5); @@ -22,7 +24,7 @@ const MAX_ALLOWED_TIME_DRIFT: TimestampInSeconds = TimestampInSeconds(5); pub struct CredentialsVerification { purpose_keys_verification: Arc, verifying_vault: Arc, - identities_repository: Arc, + identities_attributes_repository: Arc, } impl CredentialsVerification { @@ -30,19 +32,14 @@ impl CredentialsVerification { pub fn new( purpose_keys_verification: Arc, verifying_vault: Arc, - identities_repository: Arc, + identities_attributes_repository: Arc, ) -> Self { Self { purpose_keys_verification, verifying_vault, - identities_repository, + identities_attributes_repository, } } - - /// [`IdentitiesRepository`] - pub fn identities_repository(&self) -> Arc { - self.identities_repository.clone() - } } impl CredentialsVerification { @@ -54,6 +51,7 @@ impl CredentialsVerification { authorities: &[Identifier], credential_and_purpose_key: &CredentialAndPurposeKey, ) -> Result { + debug!("verify purpose key attestation"); let purpose_key_data = self .purpose_keys_verification .verify_purpose_key_attestation( @@ -62,20 +60,26 @@ impl CredentialsVerification { ) .await?; + debug!("verify issuer"); if !authorities.contains(&purpose_key_data.subject) { + warn!( + "unknown authority on a credential: {}. Accepted authorities: {:?}", + purpose_key_data.subject, authorities + ); return Err(IdentityError::UnknownAuthority.into()); } + debug!("verify purpose key type"); let public_key = match purpose_key_data.public_key.clone() { PurposePublicKey::SecureChannelStatic(_) => { - return Err(IdentityError::InvalidKeyType.into()) + return Err(IdentityError::InvalidKeyType.into()); } PurposePublicKey::CredentialSigning(public_key) => public_key, }; + debug!("verify signature"); let public_key = public_key.into(); - let versioned_data_hash = self .verifying_vault .sha256(&credential_and_purpose_key.credential.data) @@ -100,6 +104,10 @@ impl CredentialsVerification { let credential_data = CredentialData::get_data(&versioned_data)?; + debug!( + "verify subject {:?}. Expected {:?}", + credential_data.subject, expected_subject + ); if credential_data.subject.is_none() { // Currently unsupported return Err(IdentityError::CredentialVerificationFailed.into()); @@ -116,6 +124,7 @@ impl CredentialsVerification { return Err(IdentityError::CredentialVerificationFailed.into()); } + debug!("verify dates"); if credential_data.created_at < purpose_key_data.created_at { // Credential validity time range should be inside the purpose key validity time range return Err(IdentityError::CredentialVerificationFailed.into()); @@ -184,7 +193,7 @@ impl CredentialsVerification { .map(|(k, v)| (Vec::::from(k), Vec::::from(v))) .collect(); - self.identities_repository + self.identities_attributes_repository .put_attributes( subject, AttributesEntry::new( diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/trust_context.rs b/implementations/rust/ockam/ockam_identity/src/credentials/trust_context.rs index 08c99053d88..c7747f92566 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/trust_context.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/trust_context.rs @@ -1,11 +1,11 @@ -use ockam_core::compat::string::{String, ToString}; +use ockam_core::compat::string::String; use ockam_core::compat::vec::Vec; -use ockam_core::Result; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; use ockam_node::Context; -use tracing::{debug, error}; use crate::models::{CredentialAndPurposeKey, Identifier}; -use crate::{AuthorityService, IdentityError}; +use crate::AuthorityService; /// A trust context defines which authorities are trusted to attest to which attributes, within a context. /// Our first implementation assumes that there is only one authority and it is trusted to attest to all attributes within this context. @@ -13,14 +13,18 @@ use crate::{AuthorityService, IdentityError}; pub struct TrustContext { /// This is the ID of the trust context; which is primarily used for ABAC policies id: String, - /// Authority capable of retrieving credentials - authority: Option, + + /// Authority service + authority_service: Option, } impl TrustContext { /// Create a new Trust Context - pub fn new(id: String, authority: Option) -> Self { - Self { id, authority } + pub fn new(id: String, authority_service: Option) -> Self { + Self { + id, + authority_service, + } } /// Return the ID of the Trust Context @@ -28,16 +32,20 @@ impl TrustContext { &self.id } - /// Return the Authority of the Trust Context - pub fn authority(&self) -> Result<&AuthorityService> { - self.authority - .as_ref() - .ok_or_else(|| IdentityError::UnknownAuthority.into()) + /// Return the authority identities attached to this trust context + /// There is only the possibility to have 1 at the moment + pub fn authorities(&self) -> Vec { + match self.authority_identifier() { + Some(identifier) => vec![identifier], + None => vec![], + } } - /// Return the authority identities attached to this trust context - pub async fn authorities(&self) -> Result> { - Ok(vec![self.authority()?.identifier().clone()]) + /// Return the authority identifier + pub fn authority_identifier(&self) -> Option { + self.authority_service() + .ok() + .map(|a| a.identifier().clone()) } /// Return the credential for a given identity if an Authority has been defined @@ -46,27 +54,26 @@ impl TrustContext { &self, ctx: &Context, identifier: &Identifier, - ) -> Option { - match self.authority().ok() { - Some(authority) => match authority.credential(ctx, identifier).await { - Ok(credential) => { - debug!("retrieved a credential using the trust context authority"); - Some(credential) - } - Err(e) => { - error!( - "no credential could be retrieved {}, authority {}, subject {}", - e.to_string(), - authority.identifier(), - identifier - ); - None - } - }, - None => { - debug!("no authority is defined on the trust context"); - None + ) -> Result> { + match self.authority_service().ok() { + Some(authority_service) => { + Ok(Some(authority_service.credential(ctx, identifier).await?)) } + None => Ok(None), } } + + /// Return the authority service + fn authority_service(&self) -> Result { + self.authority_service.clone().ok_or_else(|| { + Error::new( + Origin::Identity, + Kind::Internal, + format!( + "no authority service has been defined for the trust context {}", + self.id + ), + ) + }) + } } diff --git a/implementations/rust/ockam/ockam_identity/src/error.rs b/implementations/rust/ockam/ockam_identity/src/error.rs index 71cf9a8bb88..9d605c48253 100644 --- a/implementations/rust/ockam/ockam_identity/src/error.rs +++ b/implementations/rust/ockam/ockam_identity/src/error.rs @@ -26,6 +26,8 @@ pub enum IdentityError { UnknownAuthority, /// No CredentialsRetriever NoCredentialsRetriever, + /// No Credentials set on a trust context + NoCredentialsSet, /// Unknown version of the Credential UnknownCredentialVersion, /// Invalid data_type value for Credential diff --git a/implementations/rust/ockam/ockam_identity/src/identities/identities.rs b/implementations/rust/ockam/ockam_identity/src/identities/identities.rs index bb9579924e7..e4e0d0bdb98 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/identities.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/identities.rs @@ -1,19 +1,29 @@ -use crate::identities::{IdentitiesKeys, IdentitiesRepository}; -use crate::purpose_keys::storage::{PurposeKeysRepository, PurposeKeysStorage}; -use crate::{ - Credentials, CredentialsServer, CredentialsServerModule, Identifier, IdentitiesBuilder, - IdentitiesCreation, IdentitiesReader, IdentitiesStorage, Identity, PurposeKeys, Vault, -}; - use ockam_core::compat::sync::Arc; use ockam_core::compat::vec::Vec; use ockam_core::Result; +#[cfg(feature = "storage")] +use crate::identities::storage::ChangeHistorySqlxDatabase; +#[cfg(feature = "storage")] +use crate::identities::storage::IdentityAttributesSqlxDatabase; +use crate::identities::{ChangeHistoryRepository, IdentitiesKeys}; +use crate::models::ChangeHistory; +use crate::purpose_keys::storage::PurposeKeysRepository; +#[cfg(feature = "storage")] +use crate::purpose_keys::storage::PurposeKeysSqlxDatabase; +#[cfg(feature = "storage")] +use crate::IdentitiesBuilder; +use crate::{ + Credentials, CredentialsServer, CredentialsServerModule, Identifier, IdentitiesCreation, + Identity, IdentityAttributesRepository, PurposeKeys, Vault, +}; + /// This struct supports all the services related to identities #[derive(Clone)] pub struct Identities { vault: Vault, - identities_repository: Arc, + change_history_repository: Arc, + identity_attributes_repository: Arc, purpose_keys_repository: Arc, } @@ -24,8 +34,13 @@ impl Identities { } /// Return the identities repository - pub fn repository(&self) -> Arc { - self.identities_repository.clone() + pub fn change_history_repository(&self) -> Arc { + self.change_history_repository.clone() + } + + /// Return the identity attributes repository + pub fn identity_attributes_repository(&self) -> Arc { + self.identity_attributes_repository.clone() } /// Return the purpose keys repository @@ -35,13 +50,14 @@ impl Identities { /// Get an [`Identity`] from the repository pub async fn get_identity(&self, identifier: &Identifier) -> Result { - let change_history = self.identities_repository.get_identity(identifier).await?; - Identity::import_from_change_history( - Some(identifier), - change_history, - self.vault.verifying_vault.clone(), - ) - .await + self.identities_creation().get_identity(identifier).await + } + + /// Return the change history of a persisted identity + pub async fn get_change_history(&self, identifier: &Identifier) -> Result { + self.identities_creation() + .get_change_history(identifier) + .await } /// Export an [`Identity`] from the repository @@ -53,7 +69,7 @@ impl Identities { pub fn purpose_keys(&self) -> Arc { Arc::new(PurposeKeys::new( self.vault.clone(), - self.identities_repository.as_identities_reader(), + self.identities_creation().clone(), self.identities_keys(), self.purpose_keys_repository.clone(), )) @@ -70,24 +86,20 @@ impl Identities { /// Return the identities creation service pub fn identities_creation(&self) -> Arc { Arc::new(IdentitiesCreation::new( - self.repository(), + self.change_history_repository(), self.vault.identity_vault.clone(), self.vault.verifying_vault.clone(), )) } - /// Return the identities reader - pub fn identities_reader(&self) -> Arc { - self.repository().as_identities_reader() - } - /// Return the identities credentials service pub fn credentials(&self) -> Arc { Arc::new(Credentials::new( self.vault.credential_vault.clone(), self.vault.verifying_vault.clone(), self.purpose_keys(), - self.identities_repository.clone(), + self.identities_creation().clone(), + self.identity_attributes_repository.clone(), )) } @@ -101,22 +113,26 @@ impl Identities { /// Create a new identities module pub(crate) fn new( vault: Vault, - identities_repository: Arc, + change_history_repository: Arc, + identity_attributes_repository: Arc, purpose_keys_repository: Arc, ) -> Identities { Identities { vault, - identities_repository, + change_history_repository, + identity_attributes_repository, purpose_keys_repository, } } /// Return a default builder for identities - pub fn builder() -> IdentitiesBuilder { - IdentitiesBuilder { - vault: Vault::create(), - repository: IdentitiesStorage::create(), - purpose_keys_repository: PurposeKeysStorage::create(), - } + #[cfg(feature = "storage")] + pub async fn builder() -> Result { + Ok(IdentitiesBuilder { + vault: Vault::create().await?, + change_history_repository: ChangeHistorySqlxDatabase::create().await?, + identity_attributes_repository: IdentityAttributesSqlxDatabase::create().await?, + purpose_keys_repository: PurposeKeysSqlxDatabase::create().await?, + }) } } diff --git a/implementations/rust/ockam/ockam_identity/src/identities/identities_builder.rs b/implementations/rust/ockam/ockam_identity/src/identities/identities_builder.rs index 4cc6e9252a6..db891c677b6 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/identities_builder.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/identities_builder.rs @@ -1,27 +1,31 @@ -use crate::identities::{Identities, IdentitiesRepository, IdentitiesStorage}; -use crate::purpose_keys::storage::{PurposeKeysRepository, PurposeKeysStorage}; -use crate::storage::Storage; -use crate::{Vault, VaultStorage}; - use ockam_core::compat::sync::Arc; +#[cfg(feature = "storage")] +use ockam_core::Result; +use ockam_vault::storage::SecretsRepository; + +use crate::identities::{ChangeHistoryRepository, Identities}; +use crate::purpose_keys::storage::PurposeKeysRepository; +use crate::{IdentityAttributesRepository, Vault}; /// Builder for Identities services #[derive(Clone)] pub struct IdentitiesBuilder { pub(crate) vault: Vault, - pub(crate) repository: Arc, + pub(crate) change_history_repository: Arc, + pub(crate) identity_attributes_repository: Arc, pub(crate) purpose_keys_repository: Arc, } /// Return a default identities -pub fn identities() -> Arc { - Identities::builder().build() +#[cfg(feature = "storage")] +pub async fn identities() -> Result> { + Ok(Identities::builder().await?.build()) } impl IdentitiesBuilder { - /// With Software Vault with given Storage - pub fn with_vault_storage(mut self, storage: VaultStorage) -> Self { - self.vault = Vault::create_with_persistent_storage(storage); + /// With Software Vault with given secrets repository + pub fn with_secrets_repository(mut self, repository: Arc) -> Self { + self.vault = Vault::create_with_secrets_repository(repository); self } @@ -31,20 +35,22 @@ impl IdentitiesBuilder { self } - /// Set a specific storage for identities - pub fn with_identities_storage(self, storage: Arc) -> Self { - self.with_identities_repository(Arc::new(IdentitiesStorage::new(storage))) - } - /// Set a specific repository for identities - pub fn with_identities_repository(mut self, repository: Arc) -> Self { - self.repository = repository; + pub fn with_change_history_repository( + mut self, + repository: Arc, + ) -> Self { + self.change_history_repository = repository; self } - /// Set a specific storage for Purpose Keys - pub fn with_purpose_keys_storage(self, storage: Arc) -> Self { - self.with_purpose_keys_repository(Arc::new(PurposeKeysStorage::new(storage))) + /// Set a specific repository for identity attributes + pub fn with_identity_attributes_repository( + mut self, + repository: Arc, + ) -> Self { + self.identity_attributes_repository = repository; + self } /// Set a specific repository for Purpose Keys @@ -60,7 +66,8 @@ impl IdentitiesBuilder { pub fn build(self) -> Arc { Arc::new(Identities::new( self.vault, - self.repository, + self.change_history_repository, + self.identity_attributes_repository, self.purpose_keys_repository, )) } diff --git a/implementations/rust/ockam/ockam_identity/src/identities/identities_creation.rs b/implementations/rust/ockam/ockam_identity/src/identities/identities_creation.rs index cbc12f10673..7e88d22611f 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/identities_creation.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/identities_creation.rs @@ -1,15 +1,16 @@ use ockam_core::compat::sync::Arc; -use ockam_core::Result; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; use ockam_vault::{SigningSecretKeyHandle, VaultForSigning, VaultForVerifyingSignatures}; use crate::identities::identity_builder::IdentityBuilder; use crate::models::{ChangeHistory, Identifier}; -use crate::{IdentitiesKeys, IdentitiesRepository, Identity, IdentityError}; +use crate::{ChangeHistoryRepository, IdentitiesKeys, Identity, IdentityError}; use crate::{IdentityHistoryComparison, IdentityOptions}; /// This struct supports functions for the creation and import of identities using an IdentityVault pub struct IdentitiesCreation { - pub(super) repository: Arc, + pub(super) repository: Arc, pub(super) identity_vault: Arc, pub(super) verifying_vault: Arc, } @@ -17,7 +18,7 @@ pub struct IdentitiesCreation { impl IdentitiesCreation { /// Create a new identities import module pub fn new( - repository: Arc, + repository: Arc, identity_vault: Arc, verifying_vault: Arc, ) -> Self { @@ -37,18 +38,13 @@ impl IdentitiesCreation { } /// Import and verify identity from its binary format - /// This action persists the Identity in the storage, use `Identity::import` to avoid that pub async fn import( &self, expected_identifier: Option<&Identifier>, data: &[u8], ) -> Result { - let identity = - Identity::import(expected_identifier, data, self.verifying_vault.clone()).await?; - - self.update_identity(&identity).await?; - - Ok(identity.identifier().clone()) + self.import_from_change_history(expected_identifier, ChangeHistory::import(data)?) + .await } /// Import and verify identity from its Change History @@ -66,7 +62,6 @@ impl IdentitiesCreation { .await?; self.update_identity(&identity).await?; - Ok(identity.identifier().clone()) } @@ -92,7 +87,7 @@ impl IdentitiesCreation { ) -> Result { let identity = self.identities_keys().create_initial_key(options).await?; self.repository - .update_identity(identity.identifier(), identity.change_history()) + .store_change_history(identity.identifier(), identity.change_history().clone()) .await?; Ok(identity.identifier().clone()) } @@ -111,22 +106,14 @@ impl IdentitiesCreation { identifier: &Identifier, options: IdentityOptions, ) -> Result<()> { - let change_history = self.repository.get_identity(identifier).await?; - - let identity = Identity::import_from_change_history( - Some(identifier), - change_history, - self.verifying_vault.clone(), - ) - .await?; - + let identity = self.get_identity(identifier).await?; let identity = self .identities_keys() .rotate_key_with_options(identity, options) .await?; self.repository - .update_identity(identity.identifier(), identity.change_history()) + .store_change_history(identity.identifier(), identity.change_history().clone()) .await?; Ok(()) @@ -158,7 +145,6 @@ impl IdentitiesCreation { { return Err(IdentityError::WrongSecretKey.into()); } - Ok(identity.identifier().clone()) } @@ -171,6 +157,37 @@ impl IdentitiesCreation { pub fn verifying_vault(&self) -> Arc { self.verifying_vault.clone() } + + /// Return the change history of a persisted identity + pub async fn get_identity(&self, identifier: &Identifier) -> Result { + match self.repository.get_change_history(identifier).await? { + Some(change_history) => { + let identity = Identity::import_from_change_history( + Some(identifier), + change_history, + self.verifying_vault.clone(), + ) + .await?; + Ok(identity) + } + None => Err(Error::new( + Origin::Core, + Kind::NotFound, + format!("identity not found for identifier {}", identifier), + )), + } + } + /// Return the change history of a persisted identity + pub async fn get_change_history(&self, identifier: &Identifier) -> Result { + match self.repository.get_change_history(identifier).await? { + Some(change_history) => Ok(change_history), + None => Err(Error::new( + Origin::Core, + Kind::NotFound, + format!("identity not found for identifier {}", identifier), + )), + } + } } impl IdentitiesCreation { @@ -182,7 +199,7 @@ impl IdentitiesCreation { pub async fn update_identity(&self, identity: &Identity) -> Result<()> { if let Some(known_identity) = self .repository - .retrieve_identity(identity.identifier()) + .get_change_history(identity.identifier()) .await? { let known_identity = Identity::import_from_change_history( @@ -198,14 +215,17 @@ impl IdentitiesCreation { } IdentityHistoryComparison::Newer => { self.repository - .update_identity(identity.identifier(), identity.change_history()) + .store_change_history( + identity.identifier(), + identity.change_history().clone(), + ) .await?; } IdentityHistoryComparison::Equal => {} } } else { self.repository - .update_identity(identity.identifier(), identity.change_history()) + .store_change_history(identity.identifier(), identity.change_history().clone()) .await?; } diff --git a/implementations/rust/ockam/ockam_identity/src/identities/identity_keys.rs b/implementations/rust/ockam/ockam_identity/src/identities/identity_keys.rs index 6b303ebc926..d11206a2e54 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/identity_keys.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/identity_keys.rs @@ -165,7 +165,7 @@ mod test { #[ockam_macros::test] async fn test_basic_identity_key_ops(ctx: &mut Context) -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_keys = identities.identities_keys(); let key1 = identities_keys diff --git a/implementations/rust/ockam/ockam_identity/src/identities/mod.rs b/implementations/rust/ockam/ockam_identity/src/identities/mod.rs index 469b141a565..c582ab0041d 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/mod.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/mod.rs @@ -5,9 +5,7 @@ mod identities_creation; mod identity_builder; mod identity_keys; mod identity_options; - -/// Identities storage functions -pub mod storage; +mod storage; pub use identities::*; pub use identities_builder::*; diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository.rs new file mode 100644 index 00000000000..186ebee067e --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository.rs @@ -0,0 +1,26 @@ +use ockam_core::async_trait; +use ockam_core::compat::boxed::Box; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; + +use crate::models::{ChangeHistory, Identifier}; + +/// This repository stores identity change histories +#[async_trait] +pub trait ChangeHistoryRepository: Send + Sync + 'static { + /// Store an identifier with its change history + async fn store_change_history( + &self, + identifier: &Identifier, + change_history: ChangeHistory, + ) -> Result<()>; + + /// Delete a change history given its identifier + async fn delete_change_history(&self, identifier: &Identifier) -> Result<()>; + + /// Return the change history of a persisted identity + async fn get_change_history(&self, identifier: &Identifier) -> Result>; + + /// Return all the change histories + async fn get_change_histories(&self) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs new file mode 100644 index 00000000000..fa7c1e72039 --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs @@ -0,0 +1,172 @@ +use core::str::FromStr; + +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::models::{ChangeHistory, Identifier}; +use crate::ChangeHistoryRepository; + +/// Implementation of `IdentitiesRepository` trait based on an underlying database +/// using sqlx as its API, and Sqlite as its driver +#[derive(Clone)] +pub struct ChangeHistorySqlxDatabase { + database: Arc, +} + +impl ChangeHistorySqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for change history"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("change history").await?, + ))) + } +} + +#[async_trait] +impl ChangeHistoryRepository for ChangeHistorySqlxDatabase { + async fn store_change_history( + &self, + identifier: &Identifier, + change_history: ChangeHistory, + ) -> Result<()> { + let query = query("INSERT OR REPLACE INTO identity VALUES (?, ?)") + .bind(identifier.to_sql()) + .bind(change_history.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_change_history(&self, identifier: &Identifier) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + let query1 = query("DELETE FROM identity where identifier=?").bind(identifier.to_sql()); + query1.execute(&self.database.pool).await.void()?; + + let query2 = + query("DELETE FROM identity_attributes where identifier=?").bind(identifier.to_sql()); + query2.execute(&self.database.pool).await.void()?; + transaction.commit().await.void()?; + Ok(()) + } + + async fn get_change_history(&self, identifier: &Identifier) -> Result> { + let query = + query_as("SELECT * FROM identity WHERE identifier=$1").bind(identifier.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + row.map(|r| r.change_history()).transpose() + } + + async fn get_change_histories(&self) -> Result> { + let query = query_as("SELECT * FROM identity"); + let row: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + row.iter().map(|r| r.change_history()).collect() + } +} + +// Database serialization / deserialization + +impl ToSqlxType for Identifier { + fn to_sql(&self) -> SqlxType { + self.to_string().to_sql() + } +} + +impl ToSqlxType for ChangeHistory { + fn to_sql(&self) -> SqlxType { + self.export_as_string().unwrap().to_sql() + } +} + +// Low-level representation of a table row +#[derive(sqlx::FromRow)] +pub(crate) struct ChangeHistoryRow { + identifier: String, + change_history: String, +} + +impl ChangeHistoryRow { + #[allow(dead_code)] + pub(crate) fn identifier(&self) -> Result { + Identifier::from_str(&self.identifier) + } + + pub(crate) fn change_history(&self) -> Result { + ChangeHistory::import_from_string(&self.change_history) + } +} + +#[cfg(test)] +mod tests { + use crate::{identities, Identity}; + + use super::*; + + #[tokio::test] + async fn test_identities_repository() -> Result<()> { + let identity1 = create_identity().await?; + let identity2 = create_identity().await?; + let repository = create_repository().await?; + + // store and retrieve an identity + repository + .store_change_history(identity1.identifier(), identity1.change_history().clone()) + .await?; + + // the change history can be retrieved + let result = repository + .get_change_history(identity1.identifier()) + .await?; + assert_eq!(result, Some(identity1.change_history().clone())); + + // trying to retrieve a missing identity returns None + let result = repository + .get_change_history(identity2.identifier()) + .await?; + assert_eq!(result, None); + + // the repository can return the list of all change histories + repository + .store_change_history(identity2.identifier(), identity2.change_history().clone()) + .await?; + let result = repository.get_change_histories().await?; + assert_eq!( + result, + vec![ + identity1.change_history().clone(), + identity2.change_history().clone(), + ] + ); + // a change history can also be deleted from the repository + repository + .delete_change_history(identity2.identifier()) + .await?; + let result = repository + .get_change_history(identity2.identifier()) + .await?; + assert_eq!(result, None); + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(ChangeHistorySqlxDatabase::create().await?) + } + + async fn create_identity() -> Result { + let identities = identities().await?; + let identifier = identities.identities_creation().create_identity().await?; + identities.get_identity(&identifier).await + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_impl.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_impl.rs deleted file mode 100644 index f2c84584181..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_impl.rs +++ /dev/null @@ -1,170 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::string::ToString; -use ockam_core::compat::sync::Arc; -use ockam_core::compat::vec::Vec; -use ockam_core::Result; - -use crate::identity::IdentityConstants; -use crate::models::{ChangeHistory, Identifier}; -use crate::storage::{InMemoryStorage, Storage}; -use crate::utils::now; -use crate::{ - AttributesEntry, IdentitiesReader, IdentitiesRepository, IdentitiesWriter, - IdentityAttributesReader, IdentityAttributesWriter, -}; - -/// Implementation of `IdentityAttributes` trait based on an underlying `Storage` -#[derive(Clone)] -pub struct IdentitiesStorage { - storage: Arc, -} - -#[async_trait] -impl IdentitiesRepository for IdentitiesStorage { - fn as_attributes_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_attributes_writer(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_identities_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_identities_writer(&self) -> Arc { - Arc::new(self.clone()) - } -} - -impl IdentitiesStorage { - /// Create a new storage for attributes - pub fn new(storage: Arc) -> Self { - Self { storage } - } - - /// Create a new storage for attributes - pub fn create() -> Arc { - Arc::new(Self::new(InMemoryStorage::create())) - } -} - -#[async_trait] -impl IdentityAttributesReader for IdentitiesStorage { - async fn get_attributes(&self, identity_id: &Identifier) -> Result> { - let id = identity_id.to_string(); - let entry = match self - .storage - .get(&id, IdentityConstants::ATTRIBUTES_KEY) - .await? - { - Some(e) => e, - None => return Ok(None), - }; - - let entry: AttributesEntry = minicbor::decode(&entry)?; - - let now = now()?; - match entry.expires() { - Some(exp) if exp <= now => { - self.storage - .del(&id, IdentityConstants::ATTRIBUTES_KEY) - .await?; - Ok(None) - } - _ => Ok(Some(entry)), - } - } - - async fn list(&self) -> Result> { - let mut l = Vec::new(); - for id in self.storage.keys(IdentityConstants::ATTRIBUTES_KEY).await? { - let identity_identifier = Identifier::try_from(id)?; - if let Some(attrs) = self.get_attributes(&identity_identifier).await? { - l.push((identity_identifier, attrs)) - } - } - Ok(l) - } -} - -#[async_trait] -impl IdentityAttributesWriter for IdentitiesStorage { - async fn put_attributes(&self, sender: &Identifier, entry: AttributesEntry) -> Result<()> { - // TODO: Implement expiration mechanism in Storage - let entry = minicbor::to_vec(&entry)?; - - self.storage - .set( - &sender.to_string(), - IdentityConstants::ATTRIBUTES_KEY.to_string(), - entry, - ) - .await?; - - Ok(()) - } - - /// Store an attribute name/value pair for a given identity - async fn put_attribute_value( - &self, - subject: &Identifier, - attribute_name: Vec, - attribute_value: Vec, - ) -> Result<()> { - let mut attributes = match self.get_attributes(subject).await? { - Some(entry) => (*entry.attrs()).clone(), - None => BTreeMap::new(), - }; - attributes.insert(attribute_name, attribute_value); - let entry = AttributesEntry::new(attributes, now()?, None, Some(subject.clone())); - self.put_attributes(subject, entry).await - } - - async fn delete(&self, identity: &Identifier) -> Result<()> { - self.storage - .del( - identity.to_string().as_str(), - IdentityConstants::ATTRIBUTES_KEY, - ) - .await - } -} - -#[async_trait] -impl IdentitiesWriter for IdentitiesStorage { - async fn update_identity( - &self, - identifier: &Identifier, - change_history: &ChangeHistory, - ) -> Result<()> { - self.storage - .set( - &identifier.to_string(), - IdentityConstants::CHANGE_HISTORY_KEY.to_string(), - minicbor::to_vec(change_history)?, - ) - .await - } -} - -#[async_trait] -impl IdentitiesReader for IdentitiesStorage { - async fn retrieve_identity(&self, identifier: &Identifier) -> Result> { - if let Some(data) = self - .storage - .get( - &identifier.to_string(), - IdentityConstants::CHANGE_HISTORY_KEY, - ) - .await? - { - Ok(Some(minicbor::decode(&data)?)) - } else { - Ok(None) - } - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_trait.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_trait.rs deleted file mode 100644 index 0d305d25859..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/identities_repository_trait.rs +++ /dev/null @@ -1,86 +0,0 @@ -use ockam_core::compat::boxed::Box; -use ockam_core::compat::sync::Arc; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::Result; -use ockam_core::{async_trait, Error}; - -use crate::models::{ChangeHistory, Identifier}; -use crate::AttributesEntry; - -/// Repository for data related to identities: key changes and attributes -#[async_trait] -pub trait IdentitiesRepository: - IdentityAttributesReader + IdentityAttributesWriter + IdentitiesReader + IdentitiesWriter -{ - /// Restrict this repository as a reader for attributes - fn as_attributes_reader(&self) -> Arc; - - /// Restrict this repository as a writer for attributes - fn as_attributes_writer(&self) -> Arc; - - /// Restrict this repository as a reader for identities - fn as_identities_reader(&self) -> Arc; - - /// Restrict this repository as a writer for identities - fn as_identities_writer(&self) -> Arc; -} - -/// Trait implementing read access to attributes -#[async_trait] -pub trait IdentityAttributesReader: Send + Sync + 'static { - /// Get the attributes associated with the given identity identifier - async fn get_attributes(&self, identity: &Identifier) -> Result>; - - /// List all identities with their attributes - async fn list(&self) -> Result>; -} - -/// Trait implementing write access to attributes -#[async_trait] -pub trait IdentityAttributesWriter: Send + Sync + 'static { - /// Set the attributes associated with the given identity identifier. - /// Previous values gets overridden. - async fn put_attributes(&self, identity: &Identifier, entry: AttributesEntry) -> Result<()>; - - /// Store an attribute name/value pair for a given identity - async fn put_attribute_value( - &self, - subject: &Identifier, - attribute_name: Vec, - attribute_value: Vec, - ) -> Result<()>; - - /// Remove all attributes for a given identity identifier - async fn delete(&self, identity: &Identifier) -> Result<()>; -} - -/// Trait implementing write access to identities -#[async_trait] -pub trait IdentitiesWriter: Send + Sync + 'static { - /// Store changes if there are new key changes associated to that identity - async fn update_identity( - &self, - identifier: &Identifier, - change_history: &ChangeHistory, - ) -> Result<()>; -} - -/// Trait implementing read access to identiets -#[async_trait] -pub trait IdentitiesReader: Send + Sync + 'static { - /// Return a persisted identity - async fn retrieve_identity(&self, identifier: &Identifier) -> Result>; - - /// Return a persisted identity that is expected to be present and return and Error if this is not the case - async fn get_identity(&self, identifier: &Identifier) -> Result { - match self.retrieve_identity(identifier).await? { - Some(change_history) => Ok(change_history), - None => Err(Error::new( - Origin::Core, - Kind::NotFound, - format!("identity not found for identifier {}", identifier), - )), - } - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository.rs new file mode 100644 index 00000000000..a5cc718708d --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository.rs @@ -0,0 +1,30 @@ +use crate::{AttributesEntry, Identifier}; +use async_trait::async_trait; +use ockam_core::compat::boxed::Box; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; + +/// This trait supports the persistence of attributes associated to identities +#[async_trait] +pub trait IdentityAttributesRepository: Send + Sync + 'static { + /// Get the attributes associated with the given identity identifier + async fn get_attributes(&self, subject: &Identifier) -> Result>; + + /// List all identities with their attributes + async fn list_attributes_by_identifier(&self) -> Result>; + + /// Set the attributes associated with the given identity identifier. + /// Previous values gets overridden. + async fn put_attributes(&self, subject: &Identifier, entry: AttributesEntry) -> Result<()>; + + /// Store an attribute name/value pair for a given identity + async fn put_attribute_value( + &self, + subject: &Identifier, + attribute_name: Vec, + attribute_value: Vec, + ) -> Result<()>; + + /// Remove all attributes for a given identity identifier + async fn delete(&self, identity: &Identifier) -> Result<()>; +} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository_sql.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository_sql.rs new file mode 100644 index 00000000000..61b4483c953 --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/identity_attributes_repository_sql.rs @@ -0,0 +1,256 @@ +use core::str::FromStr; +use std::collections::BTreeMap; + +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::models::Identifier; +use crate::utils::now; +use crate::{AttributesEntry, IdentityAttributesRepository, TimestampInSeconds}; + +/// Implementation of `IdentitiesRepository` trait based on an underlying database +/// using sqlx as its API, and Sqlite as its driver +#[derive(Clone)] +pub struct IdentityAttributesSqlxDatabase { + database: Arc, +} + +impl IdentityAttributesSqlxDatabase { + /// Create a new database + pub fn new(database: Arc) -> Self { + debug!("create a repository for identity attributes"); + Self { database } + } + + /// Create a new in-memory database + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("identity attributes").await?, + ))) + } +} + +#[async_trait] +impl IdentityAttributesRepository for IdentityAttributesSqlxDatabase { + async fn get_attributes(&self, identity: &Identifier) -> Result> { + let query = query_as("SELECT * FROM identity_attributes WHERE identifier=$1") + .bind(identity.to_sql()); + let identity_attributes: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(identity_attributes.map(|r| r.attributes()).transpose()?) + } + + async fn list_attributes_by_identifier(&self) -> Result> { + let query = query_as("SELECT * FROM identity_attributes"); + let result: Vec = + query.fetch_all(&self.database.pool).await.into_core()?; + result + .into_iter() + .map(|r| r.identifier().and_then(|i| r.attributes().map(|a| (i, a)))) + .collect::>>() + } + + async fn put_attributes(&self, subject: &Identifier, entry: AttributesEntry) -> Result<()> { + let query = query("INSERT OR REPLACE INTO identity_attributes VALUES (?, ?, ?, ?, ?)") + .bind(subject.to_sql()) + .bind(minicbor::to_vec(entry.attrs())?.to_sql()) + .bind(entry.added().to_sql()) + .bind(entry.expires().map(|e| e.to_sql())) + .bind(entry.attested_by().map(|e| e.to_sql())); + query.execute(&self.database.pool).await.void() + } + + /// Store an attribute name/value pair for a given identity + async fn put_attribute_value( + &self, + subject: &Identifier, + attribute_name: Vec, + attribute_value: Vec, + ) -> Result<()> { + let transaction = self.database.begin().await.into_core()?; + + let mut attributes = match self.get_attributes(subject).await? { + Some(entry) => (*entry.attrs()).clone(), + None => BTreeMap::new(), + }; + attributes.insert(attribute_name, attribute_value); + let entry = AttributesEntry::new(attributes, now()?, None, Some(subject.clone())); + self.put_attributes(subject, entry).await?; + + transaction.commit().await.void() + } + + async fn delete(&self, identity: &Identifier) -> Result<()> { + let query = + query("DELETE FROM identity_attributes WHERE identifier = ?").bind(identity.to_sql()); + query.execute(&self.database.pool).await.void() + } +} + +// Database serialization / deserialization + +impl ToSqlxType for TimestampInSeconds { + fn to_sql(&self) -> SqlxType { + self.0.to_sql() + } +} + +// Low-level representation of a table row +#[derive(FromRow)] +struct IdentityAttributesRow { + identifier: String, + attributes: Vec, + added: i64, + expires: Option, + attested_by: Option, +} + +impl IdentityAttributesRow { + fn identifier(&self) -> Result { + Identifier::from_str(&self.identifier) + } + + fn attributes(&self) -> Result { + let attributes = + minicbor::decode(self.attributes.as_slice()).map_err(SqlxDatabase::map_decode_err)?; + let added = TimestampInSeconds(self.added as u64); + let expires = self.expires.map(|v| TimestampInSeconds(v as u64)); + let attested_by = self + .attested_by + .clone() + .map(|v| Identifier::from_str(&v)) + .transpose()?; + + Ok(AttributesEntry::new( + attributes, + added, + expires, + attested_by, + )) + } +} + +#[cfg(test)] +mod tests { + use crate::identities; + use std::time::Duration; + + use super::*; + + #[tokio::test] + async fn test_identities_attributes_repository() -> Result<()> { + let repository = create_repository().await?; + + // store and retrieve attributes by identity + let identifier1 = create_identity().await?; + let attributes1 = create_attributes_entry(&identifier1).await?; + let identifier2 = create_identity().await?; + let attributes2 = create_attributes_entry(&identifier2).await?; + + repository + .put_attributes(&identifier1, attributes1.clone()) + .await?; + repository + .put_attributes(&identifier2, attributes2.clone()) + .await?; + + let result = repository.get_attributes(&identifier1).await?; + assert_eq!(result, Some(attributes1.clone())); + + let result = repository.list_attributes_by_identifier().await?; + assert_eq!( + result, + vec![ + (identifier1.clone(), attributes1.clone()), + (identifier2.clone(), attributes2.clone()) + ] + ); + + // delete attributes + repository.delete(&identifier1).await?; + let result = repository.get_attributes(&identifier1).await?; + assert_eq!(result, None); + + // store just one attribute name / value + let before_adding = now()?; + repository + .put_attribute_value( + &identifier1, + "name".as_bytes().to_vec(), + "value".as_bytes().to_vec(), + ) + .await?; + + let result = repository.get_attributes(&identifier1).await?.unwrap(); + // the name/value pair is present + assert_eq!( + result.attrs().get("name".as_bytes()), + Some(&"value".as_bytes().to_vec()) + ); + // there is a timestamp showing when the attributes have been added + assert!(result.added() >= before_adding); + + // the attributes are self-attested + assert_eq!(result.attested_by(), Some(identifier1.clone())); + + // store one more attribute name / value + // Let time pass for bit to observe a timestamp update + // We need to wait at least one second since this is the granularity of the + // timestamp for tracking attributes + tokio::time::sleep(Duration::from_millis(1100)).await; + repository + .put_attribute_value( + &identifier1, + "name2".as_bytes().to_vec(), + "value2".as_bytes().to_vec(), + ) + .await?; + + let result2 = repository.get_attributes(&identifier1).await?.unwrap(); + + // both the new and the old name/value pairs are present + assert_eq!( + result2.attrs().get("name".as_bytes()), + Some(&"value".as_bytes().to_vec()) + ); + assert_eq!( + result2.attrs().get("name2".as_bytes()), + Some(&"value2".as_bytes().to_vec()) + ); + // The original timestamp has been updated + assert!(result2.added() > result.added()); + + // the attributes are still self-attested + assert_eq!(result2.attested_by(), Some(identifier1.clone())); + Ok(()) + } + + /// HELPERS + async fn create_attributes_entry(identifier: &Identifier) -> Result { + Ok(AttributesEntry::new( + BTreeMap::from([ + ("name".as_bytes().to_vec(), "alice".as_bytes().to_vec()), + ("age".as_bytes().to_vec(), "20".as_bytes().to_vec()), + ]), + TimestampInSeconds(1000), + Some(TimestampInSeconds(2000)), + Some(identifier.clone()), + )) + } + + async fn create_identity() -> Result { + let identities = identities().await?; + identities.identities_creation().create_identity().await + } + + async fn create_repository() -> Result> { + Ok(IdentityAttributesSqlxDatabase::create().await?) + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/mod.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/mod.rs index 5ac04e522a0..ba1a56d7732 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/mod.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/mod.rs @@ -1,7 +1,16 @@ +pub use attributes_entry::*; +pub use change_history_repository::*; +#[cfg(feature = "storage")] +pub use change_history_repository_sql::*; +pub use identity_attributes_repository::*; +#[cfg(feature = "storage")] +pub use identity_attributes_repository_sql::*; + mod attributes_entry; -mod identities_repository_impl; -mod identities_repository_trait; +mod change_history_repository; +mod identity_attributes_repository; -pub use attributes_entry::*; -pub use identities_repository_impl::*; -pub use identities_repository_trait::*; +#[cfg(feature = "storage")] +mod change_history_repository_sql; +#[cfg(feature = "storage")] +mod identity_attributes_repository_sql; diff --git a/implementations/rust/ockam/ockam_identity/src/identity/identity.rs b/implementations/rust/ockam/ockam_identity/src/identity/identity.rs index 217be09fa6b..74976aef95f 100644 --- a/implementations/rust/ockam/ockam_identity/src/identity/identity.rs +++ b/implementations/rust/ockam/ockam_identity/src/identity/identity.rs @@ -1,16 +1,18 @@ -use crate::models::{Change, ChangeHash, ChangeHistory, Identifier}; -use crate::verified_change::VerifiedChange; -use crate::IdentityError; -use crate::IdentityHistoryComparison; - use core::cmp::Ordering; use core::fmt; use core::fmt::{Display, Formatter}; + +use ockam_core::compat::string::String; use ockam_core::compat::sync::Arc; use ockam_core::compat::vec::Vec; use ockam_core::Result; use ockam_vault::{VaultForVerifyingSignatures, VerifyingPublicKey}; +use crate::models::{Change, ChangeHash, ChangeHistory, Identifier}; +use crate::verified_change::VerifiedChange; +use crate::IdentityHistoryComparison; +use crate::{IdentityError, Vault}; + /// Verified Identity #[derive(Clone, Debug)] pub struct Identity { @@ -76,6 +78,36 @@ impl Identity { self.change_history.export() } + /// Export an `Identity` to a hex-encoded string + pub fn export_as_string(&self) -> Result { + self.change_history.export_as_string() + } + + /// Import and verify Identity from the ChangeHistory as a string + pub async fn import_from_string( + expected_identifier: Option<&Identifier>, + change_history: &str, + vault: Arc, + ) -> Result { + let change_history = ChangeHistory::import_from_string(change_history)?; + Self::import_from_change_history(expected_identifier, change_history, vault).await + } + + /// Create an identity for its change history as a hex-encoded string + pub async fn create(change_history: &str) -> Result { + Self::import_from_string(None, change_history, Vault::create_verifying_vault()).await + } + + /// Create an identity for its change history + pub async fn create_from_change_history(change_history: &ChangeHistory) -> Result { + Self::import_from_change_history( + None, + change_history.clone(), + Vault::create_verifying_vault(), + ) + .await + } + /// Import and verify Identity from the ChangeHistory pub async fn import_from_change_history( expected_identifier: Option<&Identifier>, @@ -168,6 +200,12 @@ impl Display for Identity { let identifier = self.identifier(); writeln!(f, "Identifier: {identifier}")?; + self.change_history.fmt(f) + } +} + +impl Display for ChangeHistory { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let history = hex::encode(self.export().map_err(|_| fmt::Error)?); writeln!(f, "Change history: {history}") } @@ -175,16 +213,20 @@ impl Display for Identity { #[cfg(test)] mod tests { - use super::*; - use crate::{identities, Identities, Vault}; use core::str::FromStr; + + use rand::thread_rng; + use ockam_core::compat::rand::RngCore; use ockam_vault::{EdDSACurve25519SecretKey, SigningSecret, SoftwareVaultForSigning}; - use rand::thread_rng; + + use crate::{identities, Identities, Vault}; + + use super::*; #[tokio::test] - async fn test_display() { - let identities = identities(); + async fn test_display() -> Result<()> { + let identities = identities().await?; let data = hex::decode("81825837830101583285f68200815820f405e06d988fa8039cce1cd0ae607e46847c1b64bc459ca9d89dd9b21ae30681f41a654cebe91a7818eee98200815840494c9b70e8a9ad5593fceb478f722a513b4bd39fa70f4265d584253bc24617d0eb498ce532273f6d0d5326921e013696fce57c20cc6c4008f74b816810f0b009").unwrap(); let identifier = identities .identities_creation() @@ -205,14 +247,15 @@ mod tests { let expected = r#"Identifier: I923829d0397a06fa862be5a87b7966959b8ef99ab6455b843ca9131a747b4819 Change history: 81825837830101583285f68200815820f405e06d988fa8039cce1cd0ae607e46847c1b64bc459ca9d89dd9b21ae30681f41a654cebe91a7818eee98200815840494c9b70e8a9ad5593fceb478f722a513b4bd39fa70f4265d584253bc24617d0eb498ce532273f6d0d5326921e013696fce57c20cc6c4008f74b816810f0b009 "#; - assert_eq!(actual, expected) + assert_eq!(actual, expected); + Ok(()) } #[tokio::test] async fn test_compare() -> Result<()> { - let signing_vault0 = SoftwareVaultForSigning::create(); - let signing_vault01 = SoftwareVaultForSigning::create(); - let signing_vault02 = SoftwareVaultForSigning::create(); + let signing_vault0 = SoftwareVaultForSigning::create().await?; + let signing_vault01 = SoftwareVaultForSigning::create().await?; + let signing_vault02 = SoftwareVaultForSigning::create().await?; let mut key0_bin = [0u8; 32]; thread_rng().fill_bytes(&mut key0_bin); @@ -234,10 +277,11 @@ Change history: 81825837830101583285f68200815820f405e06d988fa8039cce1cd0ae607e46 .await?; let identities0 = Identities::builder() + .await? .with_vault(Vault::new( signing_vault0, - Vault::create_secure_channel_vault(), - Vault::create_credential_vault(), + Vault::create_secure_channel_vault().await?, + Vault::create_credential_vault().await?, Vault::create_verifying_vault(), )) .build(); @@ -252,18 +296,20 @@ Change history: 81825837830101583285f68200815820f405e06d988fa8039cce1cd0ae607e46 let identity0_bin = identities0.export_identity(&identifier0).await?; let identities01 = Identities::builder() + .await? .with_vault(Vault::new( signing_vault01, - Vault::create_secure_channel_vault(), - Vault::create_credential_vault(), + Vault::create_secure_channel_vault().await?, + Vault::create_credential_vault().await?, Vault::create_verifying_vault(), )) .build(); let identities02 = Identities::builder() + .await? .with_vault(Vault::new( signing_vault02, - Vault::create_secure_channel_vault(), - Vault::create_credential_vault(), + Vault::create_secure_channel_vault().await?, + Vault::create_credential_vault().await?, Vault::create_verifying_vault(), )) .build(); diff --git a/implementations/rust/ockam/ockam_identity/src/lib.rs b/implementations/rust/ockam/ockam_identity/src/lib.rs index 49151948155..087b78cab76 100644 --- a/implementations/rust/ockam/ockam_identity/src/lib.rs +++ b/implementations/rust/ockam/ockam_identity/src/lib.rs @@ -25,12 +25,25 @@ unused_import_braces, unused_qualifications )] #![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "std")] -extern crate core; - #[cfg(feature = "alloc")] #[macro_use] extern crate alloc; +#[cfg(feature = "std")] +extern crate core; + +/// +/// Exports +/// +pub use credentials::*; +pub use error::*; +pub use identities::*; +pub use identity::*; +pub use models::{Attributes, Credential, Identifier, TimestampInSeconds}; +pub use purpose_key::*; +pub use purpose_keys::*; +pub use secure_channel::*; +pub use secure_channels::*; +pub use vault::*; /// Utilities pub mod utils; @@ -62,23 +75,5 @@ pub mod secure_channel; /// Service supporting the creation of secure channel listener and connection to a listener pub mod secure_channels; -/// Storage functions -pub mod storage; - /// Vault pub mod vault; - -/// -/// Exports -/// -pub use credentials::*; -pub use error::*; -pub use identities::*; -pub use identity::*; -pub use purpose_key::*; -pub use purpose_keys::*; -pub use secure_channel::*; -pub use secure_channels::*; -pub use vault::*; - -pub use models::{Attributes, Credential, Identifier, TimestampInSeconds}; diff --git a/implementations/rust/ockam/ockam_identity/src/models/credential_and_purpose_key.rs b/implementations/rust/ockam/ockam_identity/src/models/credential_and_purpose_key.rs index 70c977d014f..a9cf6702186 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/credential_and_purpose_key.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/credential_and_purpose_key.rs @@ -1,6 +1,13 @@ -use crate::models::{Credential, PurposeKeyAttestation}; use minicbor::{Decode, Encode}; +use ockam_core::compat::string::String; +use ockam_core::compat::vec::Vec; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; + +use crate::alloc::string::ToString; +use crate::models::{Credential, CredentialData, PurposeKeyAttestation}; + /// [`Credential`] and the corresponding [`PurposeKeyAttestation`] that was used to issue that /// [`Credential`] and will be used to verify it #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] @@ -12,3 +19,83 @@ pub struct CredentialAndPurposeKey { /// [`Credential`] and will be used to verify it #[n(1)] pub purpose_key_attestation: PurposeKeyAttestation, } + +impl CredentialAndPurposeKey { + /// Encode the credential as a hex String + pub fn encode_as_string(&self) -> Result { + Ok(hex::encode(self.encode_as_cbor_bytes()?)) + } + + /// Encode the credential as a CBOR bytes + pub fn encode_as_cbor_bytes(&self) -> Result> { + Ok(minicbor::to_vec(self)?) + } + + /// Decode the credential from bytes + pub fn decode_from_cbor_bytes(bytes: &[u8]) -> Result { + Ok(minicbor::decode(bytes)?) + } + + /// Decode the credential from an hex string + pub fn decode_from_string(as_hex: &str) -> Result { + let hex_decoded = hex::decode(as_hex.as_bytes()) + .map_err(|e| Error::new(Origin::Api, Kind::Serialization, e.to_string()))?; + Self::decode_from_cbor_bytes(&hex_decoded) + } + + /// Return the encoded credential data + pub fn get_credential_data(&self) -> Result { + self.credential.get_credential_data() + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::identities; + use crate::models::CredentialSchemaIdentifier; + use crate::utils::AttributesBuilder; + + use super::*; + + #[tokio::test] + async fn test_encode_decode_as_bytes() -> Result<()> { + let credential = create_credential().await?; + let decoded = CredentialAndPurposeKey::decode_from_cbor_bytes( + &credential.encode_as_cbor_bytes().unwrap(), + ); + assert!(decoded.is_ok()); + assert_eq!(decoded.unwrap(), credential); + + Ok(()) + } + + #[tokio::test] + async fn test_encode_decode_as_string() -> Result<()> { + let credential = create_credential().await?; + let decoded = + CredentialAndPurposeKey::decode_from_string(&credential.encode_as_string().unwrap()); + assert!(decoded.is_ok()); + assert_eq!(decoded.unwrap(), credential); + + Ok(()) + } + + /// HELPERS + async fn create_credential() -> Result { + let identities = identities().await?; + let issuer = identities.identities_creation().create_identity().await?; + let subject = identities.identities_creation().create_identity().await?; + + let attributes = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) + .with_attribute("name".as_bytes().to_vec(), b"value".to_vec()) + .build(); + + identities + .credentials() + .credentials_creation() + .issue_credential(&issuer, &subject, attributes, Duration::from_secs(1)) + .await + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/models/identifiers.rs b/implementations/rust/ockam/ockam_identity/src/models/identifiers.rs index 823ed0329f3..a51c7d4e406 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/identifiers.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/identifiers.rs @@ -1,5 +1,9 @@ +use core::fmt::{Debug, Formatter}; + use minicbor::{Decode, Encode}; +use crate::alloc::string::ToString; + /// Identifier length pub const IDENTIFIER_LEN: usize = 32; @@ -9,10 +13,16 @@ pub const CHANGE_HASH_LEN: usize = 32; /// Unique identifier for an [`super::super::identity::Identity`] /// Equals to the [`ChangeHash`] of the first [`super::Change`] in the [`super::ChangeHistory`] /// Computed as SHA256 of the first [`super::ChangeData`] CBOR binary -#[derive(Clone, Debug, Hash, Ord, PartialOrd, Eq, PartialEq, Encode, Decode)] +#[derive(Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Encode, Decode)] #[cbor(transparent)] pub struct Identifier(#[cbor(n(0), with = "minicbor::bytes")] pub [u8; IDENTIFIER_LEN]); +impl Debug for Identifier { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.to_string()) + } +} + /// Unique identifier for a [`super::Change`] /// Computed as SHA256 of the corresponding [`super::ChangeData`] CBOR binary #[derive(Clone, Debug, Eq, PartialEq, Encode, Decode)] diff --git a/implementations/rust/ockam/ockam_identity/src/models/utils/change_history.rs b/implementations/rust/ockam/ockam_identity/src/models/utils/change_history.rs index 17eed808b20..afd16c58233 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/utils/change_history.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/utils/change_history.rs @@ -1,12 +1,15 @@ +use ockam_core::compat::string::String; +use ockam_core::compat::vec::Vec; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{Error, Result}; +use ockam_vault::{Signature, VerifyingPublicKey}; + +use crate::alloc::string::ToString; use crate::models::{ Change, ChangeData, ChangeHistory, ChangeSignature, PrimaryPublicKey, VersionedData, CHANGE_DATA_TYPE, }; - use crate::IdentityError; -use ockam_core::compat::vec::Vec; -use ockam_core::Result; -use ockam_vault::{Signature, VerifyingPublicKey}; impl Change { /// Create [`VersionedData`] with corresponding version and data_type @@ -40,10 +43,23 @@ impl ChangeHistory { Ok(minicbor::to_vec(self)?) } + /// Export [`ChangeHistory`] to a hex encoded string + pub fn export_as_string(&self) -> Result { + Ok(hex::encode(self.export()?)) + } + /// Import [`ChangeHistory`] from a binary format using CBOR pub fn import(data: &[u8]) -> Result { Ok(minicbor::decode(data)?) } + + /// Import [`ChangeHistory`] from a hex-encoded string + pub fn import_from_string(data: &str) -> Result { + Self::import( + &hex::decode(data) + .map_err(|e| Error::new(Origin::Identity, Kind::Serialization, e.to_string()))?, + ) + } } impl From for VerifyingPublicKey { diff --git a/implementations/rust/ockam/ockam_identity/src/models/utils/credentials.rs b/implementations/rust/ockam/ockam_identity/src/models/utils/credentials.rs index 49d30d84ab4..08b14cd3446 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/utils/credentials.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/utils/credentials.rs @@ -14,6 +14,11 @@ impl Credential { data, } } + + /// Extract [`CredentialData`] + pub fn get_credential_data(&self) -> Result { + CredentialData::get_data(&minicbor::decode(&self.data)?) + } } impl CredentialData { diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_creation.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_creation.rs index 2ac3b59462f..0d51edcad4a 100644 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_creation.rs +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_creation.rs @@ -1,4 +1,5 @@ use ockam_core::compat::sync::Arc; +use ockam_core::errcode::{Kind, Origin}; use ockam_core::{Error, Result}; use crate::models::{ @@ -6,7 +7,7 @@ use crate::models::{ }; use crate::purpose_keys::storage::PurposeKeysRepository; use crate::{ - CredentialPurposeKey, CredentialPurposeKeyBuilder, IdentitiesKeys, IdentitiesReader, Identity, + CredentialPurposeKey, CredentialPurposeKeyBuilder, IdentitiesCreation, IdentitiesKeys, IdentityError, Purpose, PurposeKeyVerification, SecureChannelPurposeKey, SecureChannelPurposeKeyBuilder, TimestampInSeconds, Vault, }; @@ -15,7 +16,7 @@ use crate::{ #[derive(Clone)] pub struct PurposeKeyCreation { vault: Vault, - identities_reader: Arc, + identities_creation: Arc, identity_keys: Arc, repository: Arc, } @@ -24,13 +25,13 @@ impl PurposeKeyCreation { /// Constructor. pub(crate) fn new( vault: Vault, - identities_reader: Arc, + identities_creation: Arc, identity_keys: Arc, repository: Arc, ) -> Self { Self { vault, - identities_reader, + identities_creation, identity_keys, repository, } @@ -45,7 +46,7 @@ impl PurposeKeyCreation { pub fn purpose_keys_verification(&self) -> Arc { Arc::new(PurposeKeyVerification::new( self.vault.verifying_vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), )) } @@ -57,7 +58,7 @@ impl PurposeKeyCreation { SecureChannelPurposeKeyBuilder::new( Arc::new(Self::new( self.vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), self.identity_keys.clone(), self.repository.clone(), )), @@ -73,7 +74,7 @@ impl PurposeKeyCreation { CredentialPurposeKeyBuilder::new( Arc::new(Self::new( self.vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), self.identity_keys.clone(), self.repository.clone(), )), @@ -114,13 +115,7 @@ impl PurposeKeyCreation { created_at: TimestampInSeconds, expires_at: TimestampInSeconds, ) -> Result<(PurposeKeyAttestation, PurposeKeyAttestationData)> { - let identity_change_history = self.identities_reader.get_identity(&identifier).await?; - let identity = Identity::import_from_change_history( - Some(&identifier), - identity_change_history, - self.vault.verifying_vault.clone(), - ) - .await?; + let identity = self.identities_creation.get_identity(&identifier).await?; let attestation_data = PurposeKeyAttestationData { subject: identifier, @@ -161,7 +156,6 @@ impl PurposeKeyCreation { ) -> Result { let existent_key = async { let purpose_key_attestation = self - .repository .get_purpose_key(identifier, Purpose::SecureChannel) .await?; @@ -188,7 +182,6 @@ impl PurposeKeyCreation { ) -> Result { let existent_key = async { let purpose_key_attestation = self - .repository .get_purpose_key(identifier, Purpose::Credentials) .await?; @@ -213,7 +206,6 @@ impl PurposeKeyCreation { identifier: &Identifier, ) -> Result { let purpose_key_attestation = self - .repository .get_purpose_key(identifier, Purpose::SecureChannel) .await?; @@ -227,7 +219,6 @@ impl PurposeKeyCreation { identifier: &Identifier, ) -> Result { let purpose_key_attestation = self - .repository .get_purpose_key(identifier, Purpose::Credentials) .await?; @@ -308,3 +299,26 @@ impl PurposeKeyCreation { Ok(purpose_key) } } + +impl PurposeKeyCreation { + /// Get the [`super::super::super::purpose_key::PurposeKey`] + /// for given [`Identifier`] and [`Purpose`] + async fn get_purpose_key( + &self, + identifier: &Identifier, + purpose: Purpose, + ) -> Result { + match self + .repository() + .get_purpose_key(identifier, purpose) + .await? + { + Some(purpose_key) => Ok(purpose_key), + None => Err(Error::new( + Origin::Core, + Kind::NotFound, + format!("purpose_key not found for identifier {}", identifier), + )), + } + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_verification.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_verification.rs index 8c574f6860e..be3dffa7823 100644 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_verification.rs +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_key_verification.rs @@ -4,7 +4,7 @@ use ockam_vault::VaultForVerifyingSignatures; use crate::models::{Identifier, PurposeKeyAttestation, PurposeKeyAttestationData, VersionedData}; use crate::utils::now; -use crate::{IdentitiesReader, Identity, IdentityError, TimestampInSeconds}; +use crate::{IdentitiesCreation, IdentityError, TimestampInSeconds}; /// We allow purpose keys to be created in the future related to this machine's time due to /// possible time dyssynchronization @@ -14,18 +14,18 @@ const MAX_ALLOWED_TIME_DRIFT: TimestampInSeconds = TimestampInSeconds(5); #[derive(Clone)] pub struct PurposeKeyVerification { verifying_vault: Arc, - identities_reader: Arc, + identities_creation: Arc, } impl PurposeKeyVerification { /// Create a new identities module pub(crate) fn new( verifying_vault: Arc, - identities_reader: Arc, + identities_creation: Arc, ) -> Self { Self { verifying_vault, - identities_reader, + identities_creation, } } } @@ -53,18 +53,10 @@ impl PurposeKeyVerification { return Err(IdentityError::PurposeKeyAttestationVerificationFailed.into()); } } - - let change_history = self - .identities_reader + let identity = self + .identities_creation .get_identity(&purpose_key_data.subject) .await?; - let identity = Identity::import_from_change_history( - Some(&purpose_key_data.subject), - change_history, - self.verifying_vault.clone(), - ) - .await?; - let latest_change = identity.get_latest_change()?; // TODO: We should inspect purpose_key_data.subject_latest_change_hash, the possibilities are: diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_keys.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_keys.rs index daab3d402e1..f2301647c7f 100644 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_keys.rs +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/purpose_keys.rs @@ -1,13 +1,15 @@ use ockam_core::compat::sync::Arc; use crate::purpose_keys::storage::PurposeKeysRepository; -use crate::{IdentitiesKeys, IdentitiesReader, PurposeKeyCreation, PurposeKeyVerification, Vault}; +use crate::{ + IdentitiesCreation, IdentitiesKeys, PurposeKeyCreation, PurposeKeyVerification, Vault, +}; /// This struct supports all the services related to identities #[derive(Clone)] pub struct PurposeKeys { vault: Vault, - identities_reader: Arc, + identities_creation: Arc, identity_keys: Arc, repository: Arc, } @@ -16,13 +18,13 @@ impl PurposeKeys { /// Create a new identities module pub fn new( vault: Vault, - identities_reader: Arc, + identities_creation: Arc, identity_keys: Arc, repository: Arc, ) -> Self { Self { vault, - identities_reader, + identities_creation, identity_keys, repository, } @@ -37,7 +39,7 @@ impl PurposeKeys { pub fn purpose_keys_creation(&self) -> Arc { Arc::new(PurposeKeyCreation::new( self.vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), self.identity_keys.clone(), self.repository.clone(), )) @@ -47,19 +49,20 @@ impl PurposeKeys { pub fn purpose_keys_verification(&self) -> Arc { Arc::new(PurposeKeyVerification::new( self.vault.verifying_vault.clone(), - self.identities_reader.clone(), + self.identities_creation.clone(), )) } } #[cfg(test)] mod tests { - use crate::{identities, Purpose}; use ockam_core::Result; + use crate::{identities, Purpose}; + #[tokio::test] async fn create_purpose_keys() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_creation = identities.identities_creation(); let purpose_keys = identities.purpose_keys(); @@ -90,7 +93,7 @@ mod tests { #[tokio::test] async fn test_purpose_keys_are_persisted() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_creation = identities.identities_creation(); let purpose_keys = identities.purpose_keys(); @@ -103,12 +106,12 @@ mod tests { assert!(purpose_keys .repository() - .retrieve_purpose_key(&identifier, Purpose::Credentials) + .get_purpose_key(&identifier, Purpose::Credentials) .await? .is_some()); assert!(purpose_keys .repository() - .retrieve_purpose_key(&identifier, Purpose::SecureChannel) + .get_purpose_key(&identifier, Purpose::SecureChannel) .await? .is_none()); @@ -119,7 +122,7 @@ mod tests { let key = purpose_keys .repository() - .retrieve_purpose_key(&identifier, Purpose::Credentials) + .get_purpose_key(&identifier, Purpose::Credentials) .await? .unwrap(); purpose_keys @@ -131,7 +134,7 @@ mod tests { let key = purpose_keys .repository() - .retrieve_purpose_key(&identifier, Purpose::SecureChannel) + .get_purpose_key(&identifier, Purpose::SecureChannel) .await? .unwrap(); purpose_keys @@ -148,7 +151,7 @@ mod tests { let key = purpose_keys .repository() - .retrieve_purpose_key(&identifier, Purpose::Credentials) + .get_purpose_key(&identifier, Purpose::Credentials) .await? .unwrap(); purpose_keys diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/mod.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/mod.rs index df4359d5cd4..3adfc63a4a3 100644 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/mod.rs +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/mod.rs @@ -1,5 +1,8 @@ -mod purpose_keys_repository_impl; -mod purpose_keys_repository_trait; +pub use purpose_keys_repository::*; +#[cfg(feature = "storage")] +pub use purpose_keys_repository_sql::*; -pub use purpose_keys_repository_impl::*; -pub use purpose_keys_repository_trait::*; +mod purpose_keys_repository; + +#[cfg(feature = "storage")] +mod purpose_keys_repository_sql; diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository.rs new file mode 100644 index 00000000000..ec037c52b9b --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository.rs @@ -0,0 +1,33 @@ +use ockam_core::async_trait; +use ockam_core::compat::boxed::Box; +use ockam_core::Result; + +use crate::models::{Identifier, PurposeKeyAttestation}; +use crate::Purpose; + +// TODO: Only one PurposeKey per Purpose per Identity is supported for now + +/// This repository stores [`super::super::super::purpose_key::PurposeKey`]s +#[async_trait] +pub trait PurposeKeysRepository: Send + Sync + 'static { + /// Set the [`super::super::super::purpose_key::PurposeKey`] + /// for given [`Identifier`] and [`Purpose`] overwriting existing one (if any) + async fn set_purpose_key( + &self, + subject: &Identifier, + purpose: Purpose, + purpose_key_attestation: &PurposeKeyAttestation, + ) -> Result<()>; + + /// Delete the [`super::super::super::purpose_key::PurposeKey`] + /// for given [`Identifier`] and [`Purpose`] + async fn delete_purpose_key(&self, subject: &Identifier, purpose: Purpose) -> Result<()>; + + /// Retrieve the [`super::super::super::purpose_key::PurposeKey`] + /// for given [`Identifier`] and [`Purpose`] + async fn get_purpose_key( + &self, + identifier: &Identifier, + purpose: Purpose, + ) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_impl.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_impl.rs deleted file mode 100644 index 3cbd77f20bf..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_impl.rs +++ /dev/null @@ -1,91 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::string::{String, ToString}; -use ockam_core::compat::sync::Arc; -use ockam_core::Result; - -use crate::identity::IdentityConstants; -use crate::models::{Identifier, PurposeKeyAttestation}; -use crate::purpose_keys::storage::{PurposeKeysReader, PurposeKeysRepository, PurposeKeysWriter}; -use crate::storage::{InMemoryStorage, Storage}; -use crate::Purpose; - -/// Storage for own [`super::super::super::purpose_key::PurposeKey`]s -#[derive(Clone)] -pub struct PurposeKeysStorage { - storage: Arc, -} - -#[async_trait] -impl PurposeKeysRepository for PurposeKeysStorage { - fn as_reader(&self) -> Arc { - Arc::new(self.clone()) - } - - fn as_writer(&self) -> Arc { - Arc::new(self.clone()) - } -} - -impl PurposeKeysStorage { - /// Create a new Storage - pub fn new(storage: Arc) -> Self { - Self { storage } - } - - /// Create a new in-memory Storage - pub fn create() -> Arc { - Arc::new(Self::new(InMemoryStorage::create())) - } - - fn key(purpose: Purpose) -> String { - let key = match purpose { - Purpose::SecureChannel => IdentityConstants::SECURE_CHANNEL_PURPOSE_KEY, - Purpose::Credentials => IdentityConstants::CREDENTIALS_PURPOSE_KEY, - }; - - key.to_string() - } -} - -#[async_trait] -impl PurposeKeysWriter for PurposeKeysStorage { - async fn set_purpose_key( - &self, - subject: &Identifier, - purpose: Purpose, - purpose_key_attestation: &PurposeKeyAttestation, - ) -> Result<()> { - let key = Self::key(purpose); - self.storage - .set( - &subject.to_string(), - key.to_string(), - minicbor::to_vec(purpose_key_attestation)?, - ) - .await - } - - async fn delete_purpose_key(&self, subject: &Identifier, purpose: Purpose) -> Result<()> { - let key = Self::key(purpose); - self.storage - .del(&subject.to_string(), &key.to_string()) - .await - } -} - -#[async_trait] -impl PurposeKeysReader for PurposeKeysStorage { - async fn retrieve_purpose_key( - &self, - identifier: &Identifier, - purpose: Purpose, - ) -> Result> { - let key = Self::key(purpose); - if let Some(data) = self.storage.get(&identifier.to_string(), &key).await? { - Ok(Some(minicbor::decode(&data)?)) - } else { - Ok(None) - } - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_sql.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_sql.rs new file mode 100644 index 00000000000..f9ed624820b --- /dev/null +++ b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_sql.rs @@ -0,0 +1,183 @@ +use core::str::FromStr; + +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::string::{String, ToString}; +use ockam_core::compat::sync::Arc; +use ockam_core::compat::vec::Vec; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::identity::IdentityConstants; +use crate::models::{Identifier, PurposeKeyAttestation}; +use crate::purpose_keys::storage::PurposeKeysRepository; +use crate::Purpose; + +/// Storage for own [`super::super::super::purpose_key::PurposeKey`]s +#[derive(Clone)] +pub struct PurposeKeysSqlxDatabase { + database: Arc, +} + +impl PurposeKeysSqlxDatabase { + /// Create a new database for purpose keys + pub fn new(database: Arc) -> Self { + debug!("create a repository for purpose keys"); + Self { database } + } + + /// Create a new in-memory database for purpose keys + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("purpose keys").await?, + ))) + } +} + +#[async_trait] +impl PurposeKeysRepository for PurposeKeysSqlxDatabase { + async fn set_purpose_key( + &self, + subject: &Identifier, + purpose: Purpose, + purpose_key_attestation: &PurposeKeyAttestation, + ) -> Result<()> { + let query = query("INSERT OR REPLACE INTO purpose_key VALUES (?, ?, ?)") + .bind(subject.to_sql()) + .bind(purpose.to_sql()) + .bind(minicbor::to_vec(purpose_key_attestation)?.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_purpose_key(&self, subject: &Identifier, purpose: Purpose) -> Result<()> { + let query = query("DELETE FROM purpose_key WHERE identifier = ? and purpose = ?") + .bind(subject.to_sql()) + .bind(purpose.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn get_purpose_key( + &self, + identifier: &Identifier, + purpose: Purpose, + ) -> Result> { + let query = query_as("SELECT * FROM purpose_key WHERE identifier=$1 and purpose=$2") + .bind(identifier.to_sql()) + .bind(purpose.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.purpose_key_attestation()).transpose()?) + } +} + +// Database serialization / deserialization + +#[derive(FromRow)] +pub(crate) struct PurposeKeyRow { + // The identifier who is using this key + identifier: String, + // Purpose of the key (signing, encrypting, etc...) + purpose: String, + // Attestation that this key is valid + purpose_key_attestation: Vec, +} + +impl PurposeKeyRow { + #[allow(dead_code)] + pub(crate) fn identifier(&self) -> Result { + Identifier::from_str(&self.identifier) + } + + #[allow(dead_code)] + pub(crate) fn purpose(&self) -> Result { + match self.purpose.as_str() { + IdentityConstants::SECURE_CHANNEL_PURPOSE_KEY => Ok(Purpose::SecureChannel), + IdentityConstants::CREDENTIALS_PURPOSE_KEY => Ok(Purpose::Credentials), + _ => Err(ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + format!("unknown purpose {}", self.purpose), + )), + } + } + + pub(crate) fn purpose_key_attestation(&self) -> Result { + Ok(minicbor::decode(self.purpose_key_attestation.as_slice())?) + } +} + +impl ToSqlxType for Purpose { + fn to_sql(&self) -> SqlxType { + match self { + Purpose::SecureChannel => { + SqlxType::Text(IdentityConstants::SECURE_CHANNEL_PURPOSE_KEY.to_string()) + } + Purpose::Credentials => { + SqlxType::Text(IdentityConstants::CREDENTIALS_PURPOSE_KEY.to_string()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identities; + use crate::models::PurposeKeyAttestationSignature; + use ockam_vault::ECDSASHA256CurveP256Signature; + + #[tokio::test] + async fn test_purpose_keys_repository() -> Result<()> { + let repository = create_repository().await?; + + // A purpose key can be stored and retrieved, given the owning identifier and purpose type + let identity1 = create_identity().await?; + let attestation1 = PurposeKeyAttestation { + data: vec![1, 2, 3], + signature: PurposeKeyAttestationSignature::ECDSASHA256CurveP256( + ECDSASHA256CurveP256Signature([1; 64]), + ), + }; + repository + .set_purpose_key(&identity1, Purpose::Credentials, &attestation1) + .await?; + + let result = repository + .get_purpose_key(&identity1, Purpose::Credentials) + .await?; + assert_eq!(result, Some(attestation1)); + + // the attestation can be updated + let attestation2 = PurposeKeyAttestation { + data: vec![4, 5, 6], + signature: PurposeKeyAttestationSignature::ECDSASHA256CurveP256( + ECDSASHA256CurveP256Signature([1; 64]), + ), + }; + repository + .set_purpose_key(&identity1, Purpose::Credentials, &attestation2) + .await?; + + let result = repository + .get_purpose_key(&identity1, Purpose::Credentials) + .await?; + assert_eq!(result, Some(attestation2)); + + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(PurposeKeysSqlxDatabase::create().await?) + } + + async fn create_identity() -> Result { + let identities = identities().await?; + identities.identities_creation().create_identity().await + } +} diff --git a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_trait.rs b/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_trait.rs deleted file mode 100644 index 3ecbae40295..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/purpose_keys/storage/purpose_keys_repository_trait.rs +++ /dev/null @@ -1,65 +0,0 @@ -use ockam_core::compat::boxed::Box; -use ockam_core::compat::sync::Arc; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::Result; -use ockam_core::{async_trait, Error}; - -use crate::models::{Identifier, PurposeKeyAttestation}; -use crate::Purpose; - -// TODO: Only one PurposeKey per Purpose per Identity is supported for now - -/// Storage for [`super::super::super::purpose_key::PurposeKey`]s -#[async_trait] -pub trait PurposeKeysRepository: PurposeKeysReader + PurposeKeysWriter { - /// Return the read access to the Storage - fn as_reader(&self) -> Arc; - /// Return the write access to the Storage - fn as_writer(&self) -> Arc; -} - -/// Write access to [`super::super::super::purpose_key::PurposeKey`]s' Storage -#[async_trait] -pub trait PurposeKeysWriter: Send + Sync + 'static { - /// Set the [`super::super::super::purpose_key::PurposeKey`] - /// for given [`Identifier`] and [`Purpose`] overwriting existing one (if any) - async fn set_purpose_key( - &self, - subject: &Identifier, - purpose: Purpose, - purpose_key_attestation: &PurposeKeyAttestation, - ) -> Result<()>; - - /// Delete the [`super::super::super::purpose_key::PurposeKey`] - /// for given [`Identifier`] and [`Purpose`] - async fn delete_purpose_key(&self, subject: &Identifier, purpose: Purpose) -> Result<()>; -} - -/// Read access to [`super::super::super::purpose_key::PurposeKey`]s' Storage -#[async_trait] -pub trait PurposeKeysReader: Send + Sync + 'static { - /// Retrieve the [`super::super::super::purpose_key::PurposeKey`] - /// for given [`Identifier`] and [`Purpose`] - async fn retrieve_purpose_key( - &self, - identifier: &Identifier, - purpose: Purpose, - ) -> Result>; - - /// Get the [`super::super::super::purpose_key::PurposeKey`] - /// for given [`Identifier`] and [`Purpose`] - async fn get_purpose_key( - &self, - identifier: &Identifier, - purpose: Purpose, - ) -> Result { - match self.retrieve_purpose_key(identifier, purpose).await? { - Some(purpose_key) => Ok(purpose_key), - None => Err(Error::new( - Origin::Core, - Kind::NotFound, - format!("purpose_key not found for identifier {}", identifier), - )), - } - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/access_control/credential_access_control.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/access_control/credential_access_control.rs index 8b532ba83fa..908eb1c0d00 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/access_control/credential_access_control.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/access_control/credential_access_control.rs @@ -4,25 +4,25 @@ use ockam_core::compat::{boxed::Box, sync::Arc, vec::Vec}; use ockam_core::Result; use ockam_core::{async_trait, RelayMessage}; -use crate::identities::IdentitiesRepository; use crate::secure_channel::local_info::IdentitySecureChannelLocalInfo; +use crate::IdentityAttributesRepository; /// Access control checking that message senders have a specific set of attributes #[derive(Clone)] pub struct CredentialAccessControl { required_attributes: Vec<(Vec, Vec)>, - storage: Arc, + identity_attributes_repository: Arc, } impl CredentialAccessControl { /// Create a new credential access control pub fn new( required_attributes: &[(Vec, Vec)], - storage: Arc, + identity_attributes_repository: Arc, ) -> Self { Self { required_attributes: required_attributes.to_vec(), - storage, + identity_attributes_repository, } } } @@ -44,7 +44,7 @@ impl IncomingAccessControl for CredentialAccessControl { IdentitySecureChannelLocalInfo::find_info(relay_message.local_message()) { let attributes = match self - .storage + .identity_attributes_repository .get_attributes(&msg_identity_id.their_identity_id()) .await? { diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs index d747ae2372c..ed33273eb8a 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/encryptor_worker.rs @@ -1,12 +1,15 @@ use core::cmp::max; + +use tracing::{debug, error, info}; + use ockam_core::compat::boxed::Box; use ockam_core::compat::sync::Arc; use ockam_core::compat::time::Duration; use ockam_core::compat::vec::Vec; -use ockam_core::{async_trait, Decodable, Route}; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::{async_trait, Decodable, Error, Route}; use ockam_core::{Any, Result, Routed, Worker}; use ockam_node::{Context, DelayedEvent}; -use tracing::{debug, error, info}; use crate::models::{CredentialData, VersionedData}; use crate::secure_channel::addresses::Addresses; @@ -14,8 +17,8 @@ use crate::secure_channel::api::{EncryptionRequest, EncryptionResponse}; use crate::secure_channel::encryptor::Encryptor; use crate::utils::now; use crate::{ - AuthorityService, Identifier, IdentitiesReader, IdentityError, PlaintextPayloadMessage, - RefreshCredentialsMessage, SecureChannelMessage, TimestampInSeconds, + ChangeHistoryRepository, Identifier, IdentityError, PlaintextPayloadMessage, + RefreshCredentialsMessage, SecureChannelMessage, TimestampInSeconds, TrustContext, }; pub(crate) struct EncryptorWorker { @@ -25,7 +28,7 @@ pub(crate) struct EncryptorWorker { remote_route: Route, encryptor: Encryptor, my_identifier: Identifier, - identities_reader: Arc, + change_history_repository: Arc, /// Expiration timestamp of the credential we presented (or the soonest if there are multiple) min_credential_expiration: Option, /// The smallest interval of querying for a new credential from the credential retriever. @@ -36,7 +39,7 @@ pub(crate) struct EncryptorWorker { refresh_credential_time_gap: Duration, credential_refresh_event: Option>, // TODO: Should be CredentialsRetriever - credentials_retriever: Option, + trust_context: Option, } impl EncryptorWorker { @@ -47,11 +50,11 @@ impl EncryptorWorker { remote_route: Route, encryptor: Encryptor, my_identifier: Identifier, - identities_reader: Arc, + change_history_repository: Arc, min_credential_expiration: Option, min_credential_refresh_interval: Duration, refresh_credential_time_gap: Duration, - credentials_retriever: Option, + trust_context: Option, ) -> Self { Self { role, @@ -59,12 +62,12 @@ impl EncryptorWorker { remote_route, encryptor, my_identifier, - identities_reader, + change_history_repository, min_credential_expiration, min_credential_refresh_interval, refresh_credential_time_gap, credential_refresh_event: None, - credentials_retriever, + trust_context, } } @@ -171,16 +174,34 @@ impl EncryptorWorker { ); let change_history = self - .identities_reader - .get_identity(&self.my_identifier) - .await?; - - let credential = if let Some(credentials_retriever) = &self.credentials_retriever { - match credentials_retriever - .credential(ctx, &self.my_identifier) - .await - { - Ok(credential) => credential, + .change_history_repository + .get_change_history(&self.my_identifier) + .await? + .ok_or_else(|| { + Error::new( + Origin::Api, + Kind::NotFound, + format!( + "no change history found for identifier {}", + self.my_identifier + ), + ) + })?; + + let credential = if let Some(trust_context) = &self.trust_context { + match trust_context.get_credential(ctx, &self.my_identifier).await { + Ok(Some(credential)) => credential, + // TODO: remove the duplication with the next case when reworking the trust contexts + Ok(None) => { + info!( + "Credentials refresh failed for {} and is rescheduled in {} seconds", + self.addresses.encryptor, + self.min_credential_refresh_interval.as_secs() + ); + // Will schedule a refresh in self.min_credential_refresh_interval + self.schedule_credentials_refresh(ctx, true).await?; + return Err(IdentityError::NoCredentialsSet.into()); + } Err(err) => { info!( "Credentials refresh failed for {} and is rescheduled in {} seconds", diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs index 3f9f3f5861f..e6057654a1a 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake.rs @@ -600,13 +600,13 @@ mod tests { use super::*; use hex::decode; use ockam_core::Result; - use ockam_node::InMemoryKeyValueStorage; + use ockam_vault::storage::SecretsSqlxDatabase; use ockam_vault::{SoftwareVaultForSecureChannels, X25519SecretKey}; #[tokio::test] async fn test_initialization() -> Result<()> { let vault = Arc::new(SoftwareVaultForSecureChannels::new( - InMemoryKeyValueStorage::create(), + SecretsSqlxDatabase::create().await?, )); let static_key = vault.generate_static_x25519_secret_key().await?; @@ -688,7 +688,7 @@ mod tests { } async fn check_handshake(messages: HandshakeMessages) -> Result<()> { - let vault = SoftwareVaultForSecureChannels::create(); + let vault = SoftwareVaultForSecureChannels::create().await?; let initiator_static_key_id = vault .import_static_x25519_secret(messages.initiator_static_key) diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_state_machine.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_state_machine.rs index 5ce149006f7..5f5bc967e96 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_state_machine.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_state_machine.rs @@ -1,16 +1,18 @@ use minicbor::{Decode, Encode}; +use tracing::{debug, warn}; + +use ockam_core::compat::boxed::Box; use ockam_core::compat::string::ToString; use ockam_core::compat::sync::Arc; -use ockam_core::compat::{boxed::Box, vec::Vec}; +use ockam_core::compat::vec::Vec; use ockam_core::{async_trait, Result}; use ockam_vault::{AeadSecretKeyHandle, X25519PublicKey}; -use tracing::{debug, warn}; use crate::models::{ - ChangeHistory, CredentialAndPurposeKey, Identifier, PurposeKeyAttestation, PurposePublicKey, + ChangeHistory, CredentialAndPurposeKey, PurposeKeyAttestation, PurposePublicKey, }; use crate::{ - Identities, Identity, IdentityError, SecureChannelTrustInfo, TrustContext, TrustPolicy, + Identifier, Identities, IdentityError, SecureChannelTrustInfo, TrustContext, TrustPolicy, }; /// Interface for a state machine in a key exchange protocol @@ -100,11 +102,7 @@ impl CommonStateMachine { /// pub(super) async fn make_identity_payload(&self) -> Result> { // prepare the payload that will be sent either in message 2 or message 3 - let change_history = self - .identities - .repository() - .get_identity(&self.identifier) - .await?; + let change_history = self.identities.get_change_history(&self.identifier).await?; let payload = IdentityAndCredentials { change_history, purpose_key_attestation: self.purpose_key_attestation.clone(), @@ -169,26 +167,16 @@ impl CommonStateMachine { // Has value if it's the identity payload during the handshake and not credential refresh peer_public_key: Option<(PurposeKeyAttestation, X25519PublicKey)>, ) -> Result { - let identity = Identity::import_from_change_history( - expected_identifier.as_ref(), - change_history, - identities.vault().verifying_vault, - ) - .await?; - - identities + let their_identifier = identities .identities_creation() - .update_identity(&identity) + .import_from_change_history(expected_identifier.as_ref(), change_history.clone()) .await?; if let Some((purpose_key_attestation, peer_public_key)) = peer_public_key { let purpose_key = identities .purpose_keys() .purpose_keys_verification() - .verify_purpose_key_attestation( - Some(identity.identifier()), - &purpose_key_attestation, - ) + .verify_purpose_key_attestation(Some(&their_identifier), &purpose_key_attestation) .await?; match &purpose_key.public_key { @@ -198,13 +186,11 @@ impl CommonStateMachine { } } PurposePublicKey::CredentialSigning(_) => { - return Err(IdentityError::InvalidKeyType.into()) + return Err(IdentityError::InvalidKeyType.into()); } } } - let their_identifier = identity.identifier().clone(); - Self::verify_credentials( identities, trust_policy, @@ -251,7 +237,7 @@ impl CommonStateMachine { .credentials_verification() .receive_presented_credential( their_identifier, - &[trust_context.authority()?.identifier().clone()], + &trust_context.authorities(), credential, ) .await; diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs index dabffef8297..e9d634c4191 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/handshake/handshake_worker.rs @@ -26,7 +26,7 @@ use crate::secure_channel::handshake::initiator_state_machine::InitiatorStateMac use crate::secure_channel::handshake::responder_state_machine::ResponderStateMachine; use crate::secure_channel::{Addresses, Role}; use crate::{ - IdentitiesReader, IdentityError, SecureChannelPurposeKey, SecureChannelRegistryEntry, + ChangeHistoryRepository, IdentityError, SecureChannelPurposeKey, SecureChannelRegistryEntry, SecureChannels, TimestampInSeconds, TrustContext, TrustPolicy, }; @@ -45,7 +45,7 @@ pub(crate) struct HandshakeWorker { min_credential_expiration: Option, refresh_credential_time_gap: Duration, trust_context: Option, - identities_reader: Arc, + change_history_repository: Arc, } #[ockam_core::worker] @@ -230,7 +230,7 @@ impl HandshakeWorker { min_credential_expiration, refresh_credential_time_gap, trust_context, - identities_reader: identities.identities_reader(), + change_history_repository: identities.change_history_repository(), }; WorkerBuilder::new(worker) @@ -321,12 +321,6 @@ impl HandshakeWorker { handshake_results.their_identifier.clone(), ); - // FIXME - let credentials_retriever = self - .trust_context - .clone() - .and_then(|x| x.authority().cloned().ok()); - // create a separate encryptor worker which will be started independently { let encryptor = EncryptorWorker::new( @@ -339,11 +333,11 @@ impl HandshakeWorker { self.secure_channels.identities.vault().secure_channel_vault, ), self.identifier.clone(), - self.identities_reader.clone(), + self.change_history_repository.clone(), self.min_credential_expiration, self.min_credential_refresh_interval, self.refresh_credential_time_gap, - credentials_retriever, + self.trust_context.clone(), ); let next_hop = self.remote_route()?.next()?.clone(); diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channel/mod.rs b/implementations/rust/ockam/ockam_identity/src/secure_channel/mod.rs index 3670ac8a6d5..c9ac1efad61 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channel/mod.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channel/mod.rs @@ -135,8 +135,8 @@ mod tests { } async fn create_encryptor_decryptor() -> Result<(Encryptor, Decryptor)> { - let vault1 = SoftwareVaultForSecureChannels::create(); - let vault2 = SoftwareVaultForSecureChannels::create(); + let vault1 = SoftwareVaultForSecureChannels::create().await?; + let vault2 = SoftwareVaultForSecureChannels::create().await?; let mut rng = thread_rng(); let mut key = [0u8; 32]; diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs index 318d92c0891..5fdc960705c 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels.rs @@ -11,7 +11,9 @@ use crate::secure_channel::{ Addresses, Role, SecureChannelListenerOptions, SecureChannelListenerWorker, SecureChannelOptions, SecureChannelRegistry, }; -use crate::{SecureChannel, SecureChannelListener, SecureChannelsBuilder, TrustContext, Vault}; +#[cfg(feature = "storage")] +use crate::SecureChannelsBuilder; +use crate::{SecureChannel, SecureChannelListener, TrustContext, Vault}; /// Identity implementation #[derive(Clone)] @@ -48,11 +50,12 @@ impl SecureChannels { } /// Create a builder for secure channels - pub fn builder() -> SecureChannelsBuilder { - SecureChannelsBuilder { - identities_builder: Identities::builder(), + #[cfg(feature = "storage")] + pub async fn builder() -> Result { + Ok(SecureChannelsBuilder { + identities_builder: Identities::builder().await?, registry: SecureChannelRegistry::new(), - } + }) } } @@ -89,21 +92,20 @@ impl SecureChannels { trust_context: Option<&TrustContext>, ctx: &Context, ) -> Result> { - let credentials = if credentials.is_empty() { + let credential = if credentials.is_empty() { if let Some(trust_context) = trust_context { - vec![ - trust_context - .authority()? - .credential(ctx, identifier) - .await?, - ] + trust_context + .get_credential(ctx, identifier) + .await? + .into_iter() + .collect::>() } else { vec![] } } else { credentials.to_vec() }; - Ok(credentials) + Ok(credential) } /// Initiate a SecureChannel using `Route` to the SecureChannel listener and [`SecureChannelOptions`] diff --git a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs index 19b5509e5d7..ed5f28e4369 100644 --- a/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs +++ b/implementations/rust/ockam/ockam_identity/src/secure_channels/secure_channels_builder.rs @@ -1,10 +1,13 @@ use ockam_core::compat::sync::Arc; +#[cfg(feature = "storage")] +use ockam_core::Result; +use ockam_vault::storage::SecretsRepository; -use crate::identities::{Identities, IdentitiesRepository}; +use crate::identities::{ChangeHistoryRepository, Identities}; use crate::secure_channel::SecureChannelRegistry; use crate::secure_channels::SecureChannels; -use crate::storage::Storage; -use crate::{IdentitiesBuilder, Vault, VaultStorage}; +use crate::storage::PurposeKeysRepository; +use crate::{IdentitiesBuilder, IdentityAttributesRepository, Vault}; /// This struct supports all the services related to secure channels #[derive(Clone)] @@ -15,14 +18,15 @@ pub struct SecureChannelsBuilder { } /// Create default, in-memory, secure channels (mostly for examples and testing) -pub fn secure_channels() -> Arc { - SecureChannels::builder().build() +#[cfg(feature = "storage")] +pub async fn secure_channels() -> Result> { + Ok(SecureChannels::builder().await?.build()) } impl SecureChannelsBuilder { - /// With Software Vault with given Storage - pub fn with_vault_storage(mut self, storage: VaultStorage) -> Self { - self.identities_builder = self.identities_builder.with_vault_storage(storage); + /// With Software Vault with given secrets repository + pub fn with_secrets_repository(mut self, repository: Arc) -> Self { + self.identities_builder = self.identities_builder.with_secrets_repository(repository); self } @@ -32,17 +36,36 @@ impl SecureChannelsBuilder { self } - /// Set a specific storage for the identities repository - pub fn with_identities_storage(mut self, storage: Arc) -> Self { - self.identities_builder = self.identities_builder.with_identities_storage(storage); + /// Set a specific identities repository + pub fn with_change_history_repository( + mut self, + repository: Arc, + ) -> Self { + self.identities_builder = self + .identities_builder + .with_change_history_repository(repository); self } - /// Set a specific identities repository - pub fn with_identities_repository(mut self, repository: Arc) -> Self { + /// Set a specific identity attributes repository + pub fn with_identity_attributes_repository( + mut self, + repository: Arc, + ) -> Self { + self.identities_builder = self + .identities_builder + .with_identity_attributes_repository(repository); + self + } + + /// Set a specific purpose keys repository + pub fn with_purpose_keys_repository( + mut self, + repository: Arc, + ) -> Self { self.identities_builder = self .identities_builder - .with_identities_repository(repository); + .with_purpose_keys_repository(repository); self } @@ -50,7 +73,8 @@ impl SecureChannelsBuilder { pub fn with_identities(mut self, identities: Arc) -> Self { self.identities_builder = self .identities_builder - .with_identities_repository(identities.repository()) + .with_change_history_repository(identities.change_history_repository()) + .with_identity_attributes_repository(identities.identity_attributes_repository()) .with_vault(identities.vault()) .with_purpose_keys_repository(identities.purpose_keys_repository()); self diff --git a/implementations/rust/ockam/ockam_identity/src/storage/lmdb_storage.rs b/implementations/rust/ockam/ockam_identity/src/storage/lmdb_storage.rs deleted file mode 100644 index c6fb0507589..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/lmdb_storage.rs +++ /dev/null @@ -1,146 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::string::String; -use ockam_core::compat::sync::Arc; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; -use ockam_node::tokio::task::{self, JoinError}; - -use crate::storage::Storage; - -use core::str; -use lmdb::{Cursor, Database, Environment, Transaction}; -use std::fmt; -use std::path::Path; -use tokio_retry::strategy::{jitter, FixedInterval}; -use tokio_retry::Retry; -use tracing::debug; - -/// Storage using the LMDB database -#[derive(Clone)] -pub struct LmdbStorage { - /// lmdb da - pub env: Arc, - /// lmdb database file - pub map: Database, -} - -impl fmt::Debug for LmdbStorage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("Store") - } -} - -impl LmdbStorage { - /// Constructor - pub async fn new>(p: P) -> Result { - // creating a new database might be failing a few times - // if the files are currently being held by another pod which is shutting down. - // In that case we retry a few times, between 1 and 10 seconds. - let retry_strategy = FixedInterval::from_millis(1000) - .map(jitter) // add jitter to delays - .take(10); // limit to 10 retries - - let path: &Path = p.as_ref(); - Retry::spawn(retry_strategy, || async { Self::make(path).await }).await - } - - async fn make(p: &Path) -> Result { - debug!("create the LMDB database"); - std::fs::create_dir_all(p.parent().unwrap()) - .map_err(|e| Error::new(Origin::Node, Kind::Io, e))?; - let p = p.to_path_buf(); - let env = Environment::new() - .set_flags(lmdb::EnvironmentFlags::NO_SUB_DIR | lmdb::EnvironmentFlags::NO_TLS) - .set_max_dbs(1) - .open(p.as_ref()) - .map_err(map_lmdb_err)?; - let map = env - .create_db(Some("map"), lmdb::DatabaseFlags::empty()) - .map_err(map_lmdb_err)?; - Ok(LmdbStorage { - env: Arc::new(env), - map, - }) - } - - /// Write a new binary value for a given key in the database - pub async fn write(&self, k: String, v: Vec) -> Result<()> { - let d = self.clone(); - let t = move || { - let mut w = d.env.begin_rw_txn().map_err(map_lmdb_err)?; - w.put(d.map, &k, &v, lmdb::WriteFlags::empty()) - .map_err(map_lmdb_err)?; - w.commit().map_err(map_lmdb_err)?; - Ok(()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - /// Delete a database entry - pub async fn delete(&self, k: String) -> Result<()> { - let d = self.clone(); - let t = move || { - let mut w = d.env.begin_rw_txn().map_err(map_lmdb_err)?; - match w.del(d.map, &k, None) { - Ok(()) | Err(lmdb::Error::NotFound) => {} - Err(e) => return Err(map_lmdb_err(e)), - } - w.commit().map_err(map_lmdb_err)?; - Ok(()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } -} - -#[async_trait] -impl Storage for LmdbStorage { - async fn get(&self, id: &str, key: &str) -> Result>> { - let d = self.clone(); - let k = format!("{id}:{key}"); - let t = move || { - let r = d.env.begin_ro_txn().map_err(map_lmdb_err)?; - match r.get(d.map, &k) { - Ok(value) => Ok(Some(Vec::from(value))), - Err(lmdb::Error::NotFound) => Ok(None), - Err(e) => Err(map_lmdb_err(e)), - } - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn set(&self, id: &str, key: String, val: Vec) -> Result<()> { - self.write(format!("{id}:{key}"), val).await - } - - async fn del(&self, id: &str, key: &str) -> Result<()> { - self.delete(format!("{id}:{key}")).await - } - - async fn keys(&self, namespace: &str) -> Result> { - let d = self.clone(); - let suffix = format!(":{}", namespace); - let t = move || { - let r = d.env.begin_ro_txn().map_err(map_lmdb_err)?; - let mut cursor = r.open_ro_cursor(d.map).map_err(map_lmdb_err)?; - Ok(cursor - .iter() - .filter_map(|r| { - let (k, _) = r.unwrap(); - let key = str::from_utf8(k).unwrap(); - key.rsplit_once(&suffix).map(|(k, _)| k.to_string()) - }) - .collect()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_lmdb_err(err: lmdb::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} diff --git a/implementations/rust/ockam/ockam_identity/src/storage/memory.rs b/implementations/rust/ockam/ockam_identity/src/storage/memory.rs deleted file mode 100644 index fb0822837b8..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/memory.rs +++ /dev/null @@ -1,76 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::{ - boxed::Box, - collections::BTreeMap, - string::{String, ToString}, - sync::{Arc, RwLock}, - vec::Vec, -}; -use ockam_core::Result; - -use crate::storage::Storage; - -/// Non-persistent table stored in RAM -#[derive(Clone, Default)] -pub struct InMemoryStorage { - map: Arc>>, -} - -type Attributes = BTreeMap>; - -impl InMemoryStorage { - /// Constructor - pub fn new() -> Self { - Default::default() - } - - /// Constructor - pub fn create() -> Arc { - Arc::new(Self::new()) - } -} - -#[async_trait] -impl Storage for InMemoryStorage { - async fn get(&self, id: &str, namespace: &str) -> Result>> { - let m = self.map.read().unwrap(); - if let Some(a) = m.get(namespace) { - return Ok(a.get(id).cloned()); - } - Ok(None) - } - - async fn set(&self, id: &str, namespace: String, val: Vec) -> Result<()> { - let mut m = self.map.write().unwrap(); - match m.get_mut(&namespace) { - Some(a) => { - a.insert(id.to_string(), val); - } - None => { - m.insert(namespace, BTreeMap::from([(id.to_string(), val)])); - } - } - Ok(()) - } - - async fn del(&self, id: &str, namespace: &str) -> Result<()> { - let mut m = self.map.write().unwrap(); - if let Some(a) = m.get_mut(namespace) { - a.remove(id); - if a.is_empty() { - m.remove(namespace); - } - } - Ok(()) - } - - async fn keys(&self, namespace: &str) -> Result> { - Ok(self - .map - .read() - .unwrap() - .get(namespace) - .map(|m| m.keys().cloned().collect()) - .unwrap_or_default()) - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/storage/mod.rs b/implementations/rust/ockam/ockam_identity/src/storage/mod.rs deleted file mode 100644 index 4fd5b5a704c..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[allow(clippy::module_inception)] -mod storage; - -mod memory; - -/// LMDB implementation of the Storage trait -#[cfg(feature = "std")] -pub mod lmdb_storage; -/// Sqlite implementation of the Storage trait -#[cfg(feature = "sqlite")] -pub mod sqlite_storage; - -pub use memory::*; -pub use storage::*; - -#[cfg(feature = "std")] -pub use lmdb_storage::*; - -#[cfg(feature = "sqlite")] -pub use sqlite_storage::*; diff --git a/implementations/rust/ockam/ockam_identity/src/storage/sqlite_storage.rs b/implementations/rust/ockam/ockam_identity/src/storage/sqlite_storage.rs deleted file mode 100644 index 5da866126d8..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/sqlite_storage.rs +++ /dev/null @@ -1,192 +0,0 @@ -use core::str; -use ockam_core::async_trait; -use ockam_core::compat::sync::{Arc, Mutex}; -use ockam_core::compat::vec::Vec; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{Error, Result}; -use ockam_node::tokio::task::{self, JoinError}; -use rusqlite::{params, Connection}; -use std::fmt; -use std::path::Path; -use tokio_retry::strategy::{jitter, FixedInterval}; -use tokio_retry::Retry; -use tracing::debug; - -use Storage; - -/// Storage using the Sqlite database -#[derive(Clone)] -pub struct SqliteStorage { - /// Sqlite Connection - conn: Arc>, -} - -impl fmt::Debug for SqliteStorage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("SqliteStore") - } -} - -impl SqliteStorage { - const CREATE_IDENTITY_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS identity ( - id INTEGER PRIMARY KEY, - identity_id TEXT NOT NULL, - key TEXT NOT NULL, - value BLOB - );"; - const CREATE_IDENTITY_INDEX_SQL: &str = - "CREATE UNIQUE INDEX IF NOT EXISTS idx_identity_id_key ON identity (identity_id, key);"; - - const CREATE_POLICY_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS policy ( - id INTEGER PRIMARY KEY, - resource TEXT NOT NULL, - action TEXT NOT NULL, - value BLOB - );"; - const CREATE_POLICY_INDEX_SQL: &str = "CREATE UNIQUE INDEX IF NOT EXISTS idx_policy_resource_action ON policy (resource, action);"; - - /// Constructor - pub async fn new>(p: P) -> Result { - // Not sure we need this - // creating a new database might be failing a few times - // if the files are currently being held by another pod which is shutting down. - // In that case we retry a few times, between 1 and 10 seconds. - let retry_strategy = FixedInterval::from_millis(1000) - .map(jitter) // add jitter to delays - .take(10); // limit to 10 retries - - let path: &Path = p.as_ref(); - Retry::spawn(retry_strategy, || async { Self::make(path).await }).await - } - - async fn make(p: &Path) -> Result { - debug!("create the Sqlite database"); - let p = p.to_path_buf(); - // Creates database file if it doesn't exist - let conn = Connection::open(p).map_err(map_sqlite_err)?; - let _ = conn - .execute_batch( - &("PRAGMA encoding = 'UTF-8';".to_owned() - + SqliteStorage::CREATE_IDENTITY_TABLE_SQL - + SqliteStorage::CREATE_IDENTITY_INDEX_SQL - + SqliteStorage::CREATE_POLICY_TABLE_SQL - + SqliteStorage::CREATE_POLICY_INDEX_SQL), - ) - .map_err(map_sqlite_err)?; - Ok(SqliteStorage { - conn: Arc::new(Mutex::new(conn)), - }) - } - - /// Getter for Sqlite Connection - pub fn conn(&self) -> Arc> { - Arc::clone(&self.conn) - } -} - -#[async_trait] -impl Storage for SqliteStorage { - async fn get(&self, id: &str, key: &str) -> Result>> { - let conn = self.conn(); - let id = String::from(id); - let key = String::from(key); - - let t = move || { - let conn = conn.lock().unwrap(); - let result = conn - .query_row::, _, _>( - "SELECT value FROM identity WHERE identity_id = ?1 AND key = ?2;", - params![id, key], - |row| row.get(0), - ) - .map_err(map_sqlite_err)?; - Ok(Some(result)) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn set(&self, id: &str, key: String, val: Vec) -> Result<()> { - let conn = self.conn(); - let id = String::from(id); - let t = move || { - let conn = conn.lock().unwrap(); - conn.execute( - "INSERT OR REPLACE INTO identity (identity_id, key, value) VALUES (?1, ?2, ?3)", - params![id, key, val], - ) - .map_err(map_sqlite_err)?; - Ok(()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn del(&self, id: &str, key: &str) -> Result<()> { - let conn = self.conn(); - let id = String::from(id); - let key = String::from(key); - let t = move || { - let conn = conn.lock().unwrap(); - conn.execute( - "DELETE FROM identity WHERE identity_id = ?1 AND key = ?2;", - params![id, key], - ) - .map_err(map_sqlite_err)?; - Ok(()) - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } - - async fn keys(&self, namespace: &str) -> Result> { - let conn = self.conn(); - let namespace = String::from(namespace); - let t = move || { - let conn = conn.lock().unwrap(); - let mut stmt = conn - .prepare("SELECT identity_id FROM identity WHERE key = ?1;") - .map_err(map_sqlite_err)?; - let result: Result> = stmt - .query_map(params![namespace], |row| row.get(0)) - .map_err(map_sqlite_err)? - .map(|value| value.map_err(map_sqlite_err)) - .collect(); - result - }; - task::spawn_blocking(t).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_sqlite_err(err: rusqlite::Error) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -#[cfg(test)] -mod test { - use super::*; - use tempfile::NamedTempFile; - - #[tokio::test] - async fn test_basic_functionality() -> Result<()> { - let temp_path = NamedTempFile::new().unwrap().into_temp_path(); - let db = SqliteStorage::new(temp_path.to_path_buf()).await?; - - db.set("1", String::from("2"), vec![1, 2, 3, 4]).await?; - assert_eq!( - db.get("1", "2").await?, - Some(vec![1, 2, 3, 4]), - "Verify set and get" - ); - assert_eq!(db.keys("2").await?.len(), 1, "Verify keys"); - - db.set("2", String::from("2"), vec![1, 2, 3, 4]).await?; - assert_eq!(db.keys("2").await?.len(), 2, "Verify multiple keys"); - - db.del("2", "2").await?; - assert_eq!(db.keys("2").await?.len(), 1, "Verify delete"); - - Ok(()) - } -} diff --git a/implementations/rust/ockam/ockam_identity/src/storage/storage.rs b/implementations/rust/ockam/ockam_identity/src/storage/storage.rs deleted file mode 100644 index 01b664fe7c3..00000000000 --- a/implementations/rust/ockam/ockam_identity/src/storage/storage.rs +++ /dev/null @@ -1,20 +0,0 @@ -use ockam_core::async_trait; -use ockam_core::compat::{boxed::Box, string::String, vec::Vec}; -use ockam_core::Result; - -/// Storage for Authenticated data -#[async_trait] -pub trait Storage: Send + Sync + 'static { - /// Get entry - async fn get(&self, id: &str, key: &str) -> Result>>; - - /// Set entry - async fn set(&self, id: &str, key: String, val: Vec) -> Result<()>; - - /// Delete entry - async fn del(&self, id: &str, key: &str) -> Result<()>; - - /// List all keys of a given "type". TODO: we shouldn't store different things on a single - /// store. - async fn keys(&self, namespace: &str) -> Result>; -} diff --git a/implementations/rust/ockam/ockam_identity/src/vault.rs b/implementations/rust/ockam/ockam_identity/src/vault.rs index 53ccbaa8895..b6fc9133037 100644 --- a/implementations/rust/ockam/ockam_identity/src/vault.rs +++ b/implementations/rust/ockam/ockam_identity/src/vault.rs @@ -1,14 +1,16 @@ use ockam_core::compat::sync::Arc; -use ockam_node::{InMemoryKeyValueStorage, KeyValueStorage}; -use ockam_vault::legacy::{KeyId, StoredSecret}; +#[cfg(feature = "storage")] +use ockam_core::Result; +#[cfg(feature = "storage")] +use ockam_node::database::SqlxDatabase; +use ockam_vault::storage::SecretsRepository; +#[cfg(feature = "storage")] +use ockam_vault::storage::SecretsSqlxDatabase; use ockam_vault::{ SoftwareVaultForSecureChannels, SoftwareVaultForSigning, SoftwareVaultForVerifyingSignatures, VaultForSecureChannels, VaultForSigning, VaultForVerifyingSignatures, }; -/// Storage for Vault persistent values -pub type VaultStorage = Arc>; - /// Vault #[derive(Clone)] pub struct Vault { @@ -38,35 +40,39 @@ impl Vault { } } - /// Create Software implementation Vault with [`InMemoryKeyVaultStorage`] - pub fn create() -> Self { - Self::new( - Self::create_identity_vault(), - Self::create_secure_channel_vault(), - Self::create_credential_vault(), + /// Create Software implementation Vault with an in-memory storage + #[cfg(feature = "storage")] + pub async fn create() -> Result { + Ok(Self::new( + Self::create_identity_vault().await?, + Self::create_secure_channel_vault().await?, + Self::create_credential_vault().await?, Self::create_verifying_vault(), - ) + )) } - /// Create [`SoftwareVaultForSigning`] with [`InMemoryKeyVaultStorage`] - pub fn create_identity_vault() -> Arc { - Arc::new(SoftwareVaultForSigning::new( - InMemoryKeyValueStorage::create(), - )) + /// Create [`SoftwareVaultForSigning`] with an in-memory storage + #[cfg(feature = "storage")] + pub async fn create_identity_vault() -> Result> { + Ok(Arc::new(SoftwareVaultForSigning::new( + SecretsSqlxDatabase::create().await?, + ))) } - /// Create [`SoftwareSecureChannelVault`] with [`InMemoryKeyVaultStorage`] - pub fn create_secure_channel_vault() -> Arc { - Arc::new(SoftwareVaultForSecureChannels::new( - InMemoryKeyValueStorage::create(), - )) + /// Create [`SoftwareSecureChannelVault`] with an in-memory storage + #[cfg(feature = "storage")] + pub async fn create_secure_channel_vault() -> Result> { + Ok(Arc::new(SoftwareVaultForSecureChannels::new( + SecretsSqlxDatabase::create().await?, + ))) } - /// Create [`SoftwareVaultForSigning`] with [`InMemoryKeyVaultStorage`] - pub fn create_credential_vault() -> Arc { - Arc::new(SoftwareVaultForSigning::new( - InMemoryKeyValueStorage::create(), - )) + /// Create [`SoftwareVaultForSigning`] with an in-memory storage + #[cfg(feature = "storage")] + pub async fn create_credential_vault() -> Result> { + Ok(Arc::new(SoftwareVaultForSigning::new( + SecretsSqlxDatabase::create().await?, + ))) } /// Create [`SoftwareVaultForVerifyingSignatures`] @@ -76,21 +82,18 @@ impl Vault { } impl Vault { - /// Create Software Vaults with [`PersistentStorage`] with a given path - #[cfg(feature = "std")] - pub async fn create_with_persistent_storage_path( - path: &std::path::Path, - ) -> ockam_core::Result { - let storage = ockam_vault::storage::PersistentStorage::create(path).await?; - Ok(Self::create_with_persistent_storage(storage)) + /// Create Software Vaults and persist them to a sql database + #[cfg(feature = "storage")] + pub fn create_with_database(database: Arc) -> Vault { + Self::create_with_secrets_repository(Arc::new(SecretsSqlxDatabase::new(database))) } - /// Create Software Vaults with a given [`VaultStorage`]r - pub fn create_with_persistent_storage(storage: VaultStorage) -> Vault { + /// Create Software Vaults with a given secrets repository + pub fn create_with_secrets_repository(repository: Arc) -> Vault { Self::new( - Arc::new(SoftwareVaultForSigning::new(storage.clone())), - Arc::new(SoftwareVaultForSecureChannels::new(storage.clone())), - Arc::new(SoftwareVaultForSigning::new(storage)), + Arc::new(SoftwareVaultForSigning::new(repository.clone())), + Arc::new(SoftwareVaultForSecureChannels::new(repository.clone())), + Arc::new(SoftwareVaultForSigning::new(repository.clone())), Arc::new(SoftwareVaultForVerifyingSignatures {}), ) } diff --git a/implementations/rust/ockam/ockam_identity/tests/aws.rs b/implementations/rust/ockam/ockam_identity/tests/aws.rs index 36232db31dd..5cfa604dce7 100644 --- a/implementations/rust/ockam/ockam_identity/tests/aws.rs +++ b/implementations/rust/ockam/ockam_identity/tests/aws.rs @@ -16,10 +16,13 @@ use std::time::Duration; #[tokio::test] #[ignore] async fn create_identity_with_aws_pregenerated_key() -> Result<()> { - let mut vault = Vault::create(); + let mut vault = Vault::create().await?; let aws_vault = Arc::new(AwsSigningVault::create().await?); vault.identity_vault = aws_vault.clone(); - let identities = Identities::builder().with_vault(vault.clone()).build(); + let identities = Identities::builder() + .await? + .with_vault(vault.clone()) + .build(); // create a secret key using the AWS KMS let key_id = aws_vault @@ -47,10 +50,13 @@ async fn create_identity_with_aws_pregenerated_key() -> Result<()> { #[tokio::test] #[ignore] async fn create_identity_with_aws_random_key() -> Result<()> { - let mut vault = Vault::create(); + let mut vault = Vault::create().await?; let aws_vault = Arc::new(AwsSigningVault::create().await?); vault.identity_vault = aws_vault.clone(); - let identities = Identities::builder().with_vault(vault.clone()).build(); + let identities = Identities::builder() + .await? + .with_vault(vault.clone()) + .build(); let identifier = identities .identities_creation() @@ -78,10 +84,13 @@ async fn create_identity_with_aws_random_key() -> Result<()> { #[tokio::test] #[ignore] async fn create_credential_aws_key() -> Result<()> { - let mut vault = Vault::create(); + let mut vault = Vault::create().await?; let aws_vault = Arc::new(AwsSigningVault::create().await?); vault.credential_vault = aws_vault.clone(); - let identities = Identities::builder().with_vault(vault.clone()).build(); + let identities = Identities::builder() + .await? + .with_vault(vault.clone()) + .build(); let identifier = identities.identities_creation().create_identity().await?; diff --git a/implementations/rust/ockam/ockam_identity/tests/channel.rs b/implementations/rust/ockam/ockam_identity/tests/channel.rs index b9d61a7ff52..356dbfa9b3d 100644 --- a/implementations/rust/ockam/ockam_identity/tests/channel.rs +++ b/implementations/rust/ockam/ockam_identity/tests/channel.rs @@ -1,4 +1,6 @@ use core::time::Duration; +use std::sync::atomic::{AtomicU8, Ordering}; + use ockam_core::compat::sync::Arc; use ockam_core::{route, Address, AllowAll, Any, DenyAll, Mailboxes, Result, Routed, Worker}; use ockam_identity::models::{CredentialSchemaIdentifier, Identifier}; @@ -14,11 +16,10 @@ use ockam_node::{Context, MessageReceiveOptions, WorkerBuilder}; use ockam_vault::{ SoftwareVaultForSecureChannels, SoftwareVaultForSigning, SoftwareVaultForVerifyingSignatures, }; -use std::sync::atomic::{AtomicU8, Ordering}; #[ockam_macros::test] async fn test_channel(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -82,7 +83,7 @@ async fn test_channel(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn test_channel_send_credentials(context: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let authority = identities_creation.create_identity().await?; @@ -182,7 +183,7 @@ async fn test_channel_send_credentials(context: &mut Context) -> Result<()> { let alice_attributes = secure_channels .identities() - .repository() + .identity_attributes_repository() .get_attributes(&alice) .await? .unwrap(); @@ -201,7 +202,7 @@ async fn test_channel_send_credentials(context: &mut Context) -> Result<()> { let bob_attributes = secure_channels .identities() - .repository() + .identity_attributes_repository() .get_attributes(&bob) .await? .unwrap(); @@ -223,7 +224,7 @@ async fn test_channel_send_credentials(context: &mut Context) -> Result<()> { #[ockam_macros::test] async fn test_channel_rejected_trust_policy(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -280,7 +281,7 @@ async fn test_channel_rejected_trust_policy(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn test_channel_send_multiple_messages_both_directions(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -341,7 +342,7 @@ async fn test_channel_send_multiple_messages_both_directions(ctx: &mut Context) #[ockam_macros::test] async fn test_channel_registry(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -412,7 +413,7 @@ async fn test_channel_registry(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn test_channel_api(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -522,7 +523,7 @@ async fn test_channel_api(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn test_tunneled_secure_channel_works(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -597,7 +598,7 @@ async fn test_tunneled_secure_channel_works(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn test_double_tunneled_secure_channel_works(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -688,7 +689,7 @@ async fn test_double_tunneled_secure_channel_works(ctx: &mut Context) -> Result< #[ockam_macros::test] async fn test_many_times_tunneled_secure_channel_works(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -787,7 +788,7 @@ async fn access_control__known_participant__should_pass_messages(ctx: &mut Conte received_count: received_count.clone(), }; - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -837,7 +838,7 @@ async fn access_control__unknown_participant__should_not_pass_messages( received_count: received_count.clone(), }; - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; @@ -909,26 +910,32 @@ async fn access_control__no_secure_channel__should_not_pass_messages( #[ockam_macros::test] async fn test_channel_delete_ephemeral_keys(ctx: &mut Context) -> Result<()> { - let alice_identity_vault = SoftwareVaultForSigning::create(); - let alice_sc_vault = SoftwareVaultForSecureChannels::create(); + let alice_identity_vault = SoftwareVaultForSigning::create().await?; + let alice_sc_vault = SoftwareVaultForSecureChannels::create().await?; let alice_vault = Vault::new( alice_identity_vault.clone(), alice_sc_vault.clone(), - SoftwareVaultForSigning::create(), + SoftwareVaultForSigning::create().await?, SoftwareVaultForVerifyingSignatures::create(), ); - let bob_identity_vault = SoftwareVaultForSigning::create(); - let bob_sc_vault = SoftwareVaultForSecureChannels::create(); + let bob_identity_vault = SoftwareVaultForSigning::create().await?; + let bob_sc_vault = SoftwareVaultForSecureChannels::create().await?; let bob_vault = Vault::new( bob_identity_vault.clone(), bob_sc_vault.clone(), - SoftwareVaultForSigning::create(), + SoftwareVaultForSigning::create().await?, SoftwareVaultForVerifyingSignatures::create(), ); - let secure_channels_alice = SecureChannels::builder().with_vault(alice_vault).build(); - let secure_channels_bob = SecureChannels::builder().with_vault(bob_vault).build(); + let secure_channels_alice = SecureChannels::builder() + .await? + .with_vault(alice_vault) + .build(); + let secure_channels_bob = SecureChannels::builder() + .await? + .with_vault(bob_vault) + .build(); let identities_creation_alice = secure_channels_alice.identities().identities_creation(); let identities_creation_bob = secure_channels_bob.identities().identities_creation(); @@ -1031,7 +1038,7 @@ async fn test_channel_delete_ephemeral_keys(ctx: &mut Context) -> Result<()> { async fn should_stop_encryptor__and__decryptor__in__secure_channel( ctx: &mut Context, ) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let alice = identities_creation.create_identity().await?; diff --git a/implementations/rust/ockam/ockam_identity/tests/common/message_flow_auth.rs b/implementations/rust/ockam/ockam_identity/tests/common/message_flow_auth.rs index d4954a076b8..fe3f2410880 100644 --- a/implementations/rust/ockam/ockam_identity/tests/common/message_flow_auth.rs +++ b/implementations/rust/ockam/ockam_identity/tests/common/message_flow_auth.rs @@ -106,7 +106,7 @@ pub async fn create_secure_channel_listener( ctx: &Context, flow_control_id: &FlowControlId, ) -> Result { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let identifier = identities_creation.create_identity().await?; @@ -134,7 +134,7 @@ pub async fn create_secure_channel( ctx: &Context, connection: &Address, ) -> Result { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities_creation = secure_channels.identities().identities_creation(); let identifier = identities_creation.create_identity().await?; diff --git a/implementations/rust/ockam/ockam_identity/tests/credentials.rs b/implementations/rust/ockam/ockam_identity/tests/credentials.rs index 47bcb17623b..a2e4ff0649c 100644 --- a/implementations/rust/ockam/ockam_identity/tests/credentials.rs +++ b/implementations/rust/ockam/ockam_identity/tests/credentials.rs @@ -15,10 +15,10 @@ use ockam_node::{Context, WorkerBuilder}; #[ockam_macros::test] async fn full_flow_oneway(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); - let identities_repository = identities.repository(); + let identity_attributes_repository = identities.identity_attributes_repository(); let credentials = identities.credentials(); let credentials_service = identities.credentials_server(); @@ -82,7 +82,7 @@ async fn full_flow_oneway(ctx: &mut Context) -> Result<()> { .present_credential(ctx, route![channel, "credential_exchange"], credential) .await?; - let attrs = identities_repository + let attrs = identity_attributes_repository .get_attributes(&client) .await? .unwrap(); @@ -96,10 +96,10 @@ async fn full_flow_oneway(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn full_flow_twoway(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); - let identities_repository = identities.repository(); + let identity_attributes_repository = identities.identity_attributes_repository(); let credentials = identities.credentials(); let credentials_service = identities.credentials_server(); @@ -173,12 +173,12 @@ async fn full_flow_twoway(ctx: &mut Context) -> Result<()> { .present_credential_mutual( ctx, route![channel, "credential_exchange"], - trust_context.authorities().await?.as_slice(), + &trust_context.authorities(), credential, ) .await?; - let attrs1 = identities_repository + let attrs1 = identity_attributes_repository .get_attributes(&client1) .await? .unwrap(); @@ -192,7 +192,7 @@ async fn full_flow_twoway(ctx: &mut Context) -> Result<()> { b"true" ); - let attrs2 = identities_repository + let attrs2 = identity_attributes_repository .get_attributes(&client2) .await? .unwrap(); @@ -207,10 +207,10 @@ async fn full_flow_twoway(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn access_control(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); - let identities_repository = identities.repository(); + let identity_attributes_repository = identities.identity_attributes_repository(); let credentials = identities.credentials(); let credentials_service = identities.credentials_server(); @@ -275,7 +275,7 @@ async fn access_control(ctx: &mut Context) -> Result<()> { let required_attributes = vec![(b"is_superuser".to_vec(), b"true".to_vec())]; let access_control = - CredentialAccessControl::new(&required_attributes, identities_repository.clone()); + CredentialAccessControl::new(&required_attributes, identity_attributes_repository.clone()); ctx.flow_controls() .add_consumer("counter", listener.flow_control_id()); diff --git a/implementations/rust/ockam/ockam_identity/tests/credentials_refresh.rs b/implementations/rust/ockam/ockam_identity/tests/credentials_refresh.rs index 278009de2c3..fee895e258d 100644 --- a/implementations/rust/ockam/ockam_identity/tests/credentials_refresh.rs +++ b/implementations/rust/ockam/ockam_identity/tests/credentials_refresh.rs @@ -15,7 +15,7 @@ use ockam_node::Context; #[ockam_macros::test] async fn autorefresh(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); let credentials = identities.credentials(); @@ -96,7 +96,7 @@ async fn autorefresh(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn autorefresh_attributes_update(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); let credentials = identities.credentials(); @@ -152,7 +152,7 @@ async fn autorefresh_attributes_update(ctx: &mut Context) -> Result<()> { ctx.sleep(Duration::from_millis(100)).await; - let attributes_reader = identities.repository().as_attributes_reader(); + let attributes_reader = identities.identity_attributes_repository(); let added1 = attributes_reader .get_attributes(&client) @@ -188,7 +188,7 @@ async fn autorefresh_attributes_update(ctx: &mut Context) -> Result<()> { #[ockam_macros::test] async fn autorefresh_retry(ctx: &mut Context) -> Result<()> { - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let identities = secure_channels.identities(); let identities_creation = identities.identities_creation(); let credentials = identities.credentials(); diff --git a/implementations/rust/ockam/ockam_identity/tests/identity_creation.rs b/implementations/rust/ockam/ockam_identity/tests/identity_creation.rs index 12d7e15b606..14289056d51 100644 --- a/implementations/rust/ockam/ockam_identity/tests/identity_creation.rs +++ b/implementations/rust/ockam/ockam_identity/tests/identity_creation.rs @@ -1,51 +1,38 @@ use core::str::FromStr; + use ockam_core::Result; +use ockam_identity::identities; use ockam_identity::models::Identifier; -use ockam_identity::{identities, Identity}; use ockam_vault::SigningKeyType; #[tokio::test] async fn create_and_retrieve() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_creation = identities.identities_creation(); - let repository = identities.repository(); let identities_keys = identities.identities_keys(); let identifier = identities_creation.create_identity().await?; - let actual = repository.get_identity(&identifier).await?; + let actual = identities_creation.get_identity(&identifier).await?; - let actual = Identity::import_from_change_history( - Some(&identifier), - actual, - identities.vault().verifying_vault, - ) - .await?; assert_eq!( actual.identifier(), &identifier, "the identity can be retrieved from the repository" ); - let actual = repository.retrieve_identity(&identifier).await?; - assert!(actual.is_some()); - let actual = Identity::import_from_change_history( - Some(&identifier), - actual.unwrap(), - identities.vault().verifying_vault, - ) - .await?; - assert_eq!( - actual.identifier(), - &identifier, - "the identity can be retrieved from the repository as an Option" - ); + let actual = identities_creation.get_change_history(&identifier).await; + assert!(actual.is_ok()); let another_identifier = Identifier::from_str("Ie92f183eb4c324804ef4d62962dea94cf095a265a1b2c3d4e5f6a6b5c4d3e2f1")?; - let missing = repository.retrieve_identity(&another_identifier).await?; - assert_eq!(missing, None, "a missing identity returns None"); + let missing = identities_creation + .get_identity(&another_identifier) + .await + .ok(); + assert_eq!(missing, None, "a missing identity returns an error"); - let root_key = identities_keys.get_secret_key(&actual).await; + let identity = identities.get_identity(&identifier).await?; + let root_key = identities_keys.get_secret_key(&identity).await; assert!(root_key.is_ok(), "there is a key for the created identity"); Ok(()) @@ -53,9 +40,8 @@ async fn create_and_retrieve() -> Result<()> { #[tokio::test] async fn create_p256() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_creation = identities.identities_creation(); - let repository = identities.repository(); let identities_keys = identities.identities_keys(); let identifier = identities_creation @@ -63,14 +49,8 @@ async fn create_p256() -> Result<()> { .with_random_key(SigningKeyType::ECDSASHA256CurveP256) .build() .await?; - let actual = repository.get_identity(&identifier).await?; + let actual = identities_creation.get_identity(&identifier).await?; - let actual = Identity::import_from_change_history( - Some(&identifier), - actual, - identities.vault().verifying_vault, - ) - .await?; assert_eq!( actual.identifier(), &identifier, diff --git a/implementations/rust/ockam/ockam_identity/tests/identity_verification.rs b/implementations/rust/ockam/ockam_identity/tests/identity_verification.rs index 4fa1d5d3ee9..4b7f149492c 100644 --- a/implementations/rust/ockam/ockam_identity/tests/identity_verification.rs +++ b/implementations/rust/ockam/ockam_identity/tests/identity_verification.rs @@ -10,7 +10,7 @@ mod common; #[tokio::test] async fn test_valid_identity() -> Result<()> { - let identities = Identities::builder().build(); + let identities = Identities::builder().await?.build(); let identities_creation = identities.identities_creation(); let identifier = identities_creation.create_identity().await?; @@ -25,13 +25,13 @@ async fn test_valid_identity() -> Result<()> { #[tokio::test] async fn test_invalid_signature() -> Result<()> { - let mut vault = Vault::create(); + let mut vault = Vault::create().await?; let crazy_signing_vault = Arc::new(CrazySigningVault::new(0.1, vault.identity_vault)); vault.identity_vault = crazy_signing_vault.clone(); vault.verifying_vault = Arc::new(CrazyVerifyingVault { verifying_vault: vault.verifying_vault, }); - let identities = Identities::builder().with_vault(vault).build(); + let identities = Identities::builder().await?.with_vault(vault).build(); let identities_creation = identities.identities_creation(); let identifier = identities_creation.create_identity().await?; let identity = identities.get_identity(&identifier).await?; @@ -64,7 +64,7 @@ async fn test_invalid_signature() -> Result<()> { #[tokio::test] async fn test_eject_signatures() -> Result<()> { - let identities = Identities::builder().build(); + let identities = Identities::builder().await?.build(); let identities_creation = identities.identities_creation(); let identifier = identities_creation.create_identity().await?; @@ -73,8 +73,8 @@ async fn test_eject_signatures() -> Result<()> { identities_creation.rotate_identity(&identifier).await?; } - let identity = identities.repository().get_identity(&identifier).await?; - let change_history = eject_random_signature(&identity)?; + let change_history = identities.get_change_history(&identifier).await?; + let change_history = eject_random_signature(&change_history)?; let res = check_change_history(Some(&identifier), change_history).await; assert!(res.is_err()); diff --git a/implementations/rust/ockam/ockam_identity/tests/plaintext_message_flow_auth.rs b/implementations/rust/ockam/ockam_identity/tests/plaintext_message_flow_auth.rs index d960fc841a7..c1e48585513 100644 --- a/implementations/rust/ockam/ockam_identity/tests/plaintext_message_flow_auth.rs +++ b/implementations/rust/ockam/ockam_identity/tests/plaintext_message_flow_auth.rs @@ -13,8 +13,8 @@ mod common; // Bob: Secure Channel listener #[ockam_macros::test] async fn test1(ctx: &mut Context) -> Result<()> { - let alice_secure_channels = secure_channels(); - let bob_secure_channels = secure_channels(); + let alice_secure_channels = secure_channels().await?; + let bob_secure_channels = secure_channels().await?; let alice = alice_secure_channels .identities() @@ -84,8 +84,8 @@ async fn test2(ctx: &mut Context) -> Result<()> { message_should_not_pass(ctx, &connection_to_bob.clone().into()).await?; message_should_not_pass(ctx, connection_to_alice.address()).await?; - let alice_secure_channels = secure_channels(); - let bob_secure_channels = secure_channels(); + let alice_secure_channels = secure_channels().await?; + let bob_secure_channels = secure_channels().await?; let alice = alice_secure_channels .identities() @@ -113,7 +113,7 @@ async fn test2(ctx: &mut Context) -> Result<()> { ) .await?; - ctx.sleep(Duration::from_millis(50)).await; // Wait for workers to add themselves to the registry + ctx.sleep(Duration::from_millis(500)).await; // Wait for workers to add themselves to the registry let channels = bob_secure_channels .secure_channel_registry() diff --git a/implementations/rust/ockam/ockam_identity/tests/purpose_key_creation.rs b/implementations/rust/ockam/ockam_identity/tests/purpose_key_creation.rs index ca09e75a6f9..6d7b0ff8973 100644 --- a/implementations/rust/ockam/ockam_identity/tests/purpose_key_creation.rs +++ b/implementations/rust/ockam/ockam_identity/tests/purpose_key_creation.rs @@ -4,7 +4,7 @@ use ockam_vault::{SigningKeyType, VerifyingPublicKey}; #[tokio::test] async fn create_default_purpose_keys() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_creation = identities.identities_creation(); let purpose_keys = identities.purpose_keys(); @@ -58,7 +58,7 @@ async fn create_default_purpose_keys() -> Result<()> { #[tokio::test] async fn create_custom_type() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_creation = identities.identities_creation(); let purpose_keys = identities.purpose_keys(); @@ -89,7 +89,7 @@ async fn create_custom_type() -> Result<()> { #[tokio::test] async fn create_with_p256_identity() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_creation = identities.identities_creation(); let purpose_keys = identities.purpose_keys(); @@ -147,7 +147,7 @@ async fn create_with_p256_identity() -> Result<()> { #[tokio::test] async fn create_with_rotated_identity() -> Result<()> { - let identities = identities(); + let identities = identities().await?; let identities_creation = identities.identities_creation(); let purpose_keys = identities.purpose_keys(); diff --git a/implementations/rust/ockam/ockam_identity/tests/purpose_key_verification.rs b/implementations/rust/ockam/ockam_identity/tests/purpose_key_verification.rs index a4831c9b0bd..9393362683c 100644 --- a/implementations/rust/ockam/ockam_identity/tests/purpose_key_verification.rs +++ b/implementations/rust/ockam/ockam_identity/tests/purpose_key_verification.rs @@ -8,15 +8,15 @@ mod common; #[tokio::test] async fn test_invalid_signature() -> Result<()> { - let mut vault = Vault::create(); + let mut vault = Vault::create().await?; let crazy_signing_vault = Arc::new(CrazySigningVault::new(0.1, vault.identity_vault)); vault.identity_vault = crazy_signing_vault.clone(); vault.verifying_vault = Arc::new(CrazyVerifyingVault { verifying_vault: vault.verifying_vault, }); - let identities_remote = identities(); - let identities = Identities::builder().with_vault(vault).build(); + let identities_remote = identities().await?; + let identities = Identities::builder().await?.with_vault(vault).build(); let identities_creation = identities.identities_creation(); let identifier = identities_creation.create_identity().await?; let identity = identities.get_identity(&identifier).await?; diff --git a/implementations/rust/ockam/ockam_node/Cargo.toml b/implementations/rust/ockam/ockam_node/Cargo.toml index 5732a502961..4fab472ab3b 100644 --- a/implementations/rust/ockam/ockam_node/Cargo.toml +++ b/implementations/rust/ockam/ockam_node/Cargo.toml @@ -65,7 +65,7 @@ metrics = [] # message flows within Ockam apps. debugger = ["ockam_core/debugger"] -storage = ["std", "serde_json"] +storage = ["std", "time", "serde_json", "sqlx", "tokio-retry"] [dependencies] cfg-if = "1.0.0" @@ -80,7 +80,10 @@ ockam_transport_core = { path = "../ockam_transport_core", version = "^0.66.0", serde = { version = "1.0", default-features = false, features = ["derive"] } serde_bare = { version = "0.5.0", default-features = false } serde_json = { version = "1", optional = true } +sqlx = { version = "0.7.3", optional = true, features = ["sqlite", "migrate", "runtime-tokio"] } +time = { version = "0.3.30", default-features = false, optional = true } tokio = { version = "1.34", default-features = false, optional = true, features = ["sync", "time", "rt", "rt-multi-thread", "macros"] } +tokio-retry = { version = "0.3.0", optional = true } tracing = { version = "0.1", default_features = false } tracing-error = { version = "0.2", optional = true } tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"], optional = true } diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/20231006100000_create_database.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/20231006100000_create_database.sql new file mode 100644 index 00000000000..0247faf16f3 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/20231006100000_create_database.sql @@ -0,0 +1,249 @@ +-------------- +-- IDENTITIES +-------------- + +-- This table stores identities with +-- - the identity identifier (as a hex-encoded string) +-- - the encoded history of all the key rotations for this identity +CREATE TABLE identity +( + identifier TEXT NOT NULL UNIQUE, + change_history TEXT NOT NULL +); + +-- This table some local metadata about identities +CREATE TABLE named_identity +( + identifier TEXT NOT NULL UNIQUE, -- Identity identifier + name TEXT UNIQUE, -- user-specified name + vault_name TEXT NOT NULL, -- name of the vault used to store the identity keys + is_default INTEGER DEFAULT 0 -- boolean indicating if this identity is the default one (0 means true) +); + +-- This table stores the time when a given identity was enrolled +-- In the current project +CREATE TABLE identity_enrollment +( + identifier TEXT NOT NULL UNIQUE, -- Identifier of the identity + enrolled_at INTEGER NOT NULL -- UNIX timestamp in seconds +); + +-- This table lists attributes associated to a given identity +CREATE TABLE identity_attributes +( + identifier TEXT PRIMARY KEY, -- identity possessing those attributes + attributes BLOB NOT NULL, -- serialized list of attribute names and values for the identity + added INTEGER NOT NULL, -- UNIX timestamp in seconds: when those attributes were inserted in the database + expires INTEGER, -- optional UNIX timestamp in seconds: when those attributes expire + attested_by TEXT -- optional identifier which attested of these attributes +); + +-- This table stores purpose keys that have been created by a given identity +CREATE TABLE purpose_key +( + identifier TEXT NOT NULL, -- Identity identifier + purpose TEXT NOT NULL, -- Purpose of the key: SecureChannels, or Credentials + purpose_key_attestation BLOB NOT NULL -- Encoded attestation: attestation data and attestation signature +); + +CREATE UNIQUE INDEX purpose_key_index ON purpose_key (identifier, purpose); + +---------- +-- VAULTS +---------- + +-- This table stores vault metadata when several vaults have been created locally +CREATE TABLE vault +( + name TEXT PRIMARY KEY, -- User-specified name for a vault + path TEXT NOT NULL, -- Path where the vault is saved, This path can the current database path. In that case the vault data is stored in the *-secrets table below + is_default INTEGER, -- boolean indicating if this vault is the default one (0 means true) + is_kms INTEGER -- boolean indicating if this vault is a KMS one (0 means true). In that case only key handles are stored in the database +); + +-- This table stores secrets for signing data +CREATE TABLE signing_secret +( + handle BLOB PRIMARY KEY, -- Secret handle + secret_type TEXT NOT NULL, -- Secret type (EdDSACurve25519 or ECDSASHA256CurveP256) + secret BLOB NOT NULL -- Secret binary +); + +-- This table stores secrets for encrypting / decrypting data +CREATE TABLE x25519_secret +( + handle BLOB PRIMARY KEY, -- Secret handle + secret BLOB NOT NULL -- Secret binary +); + + +--------------- +-- CREDENTIALS +--------------- + +-- This table stores credentials as received by the application +CREATE TABLE credential +( + name TEXT PRIMARY KEY, -- User-defined name for this credential + issuer_identifier TEXT NOT NULL, -- Identifier of the identity which issued this credential + issuer_change_history TEXT NOT NULL, -- Change history of the identity which issued this credential (hex-encoded) + credential TEXT NOT NULL -- Encoded version of the credential: data + signature +); + +------------------ +-- TRUST CONTEXTS +------------------ + +CREATE TABLE trust_context +( + name TEXT PRIMARY KEY, -- Name for the trust context + trust_context_id TEXT NOT NULL, -- Identifier for the trust context to be used in policies + is_default INTEGER NOT NULL, -- boolean indicating if this trust context is the default one (0 means true) + credential TEXT, -- optional encoded credential which can be retrieved from this trust context + authority_change_history TEXT, -- optional: change history of the authority to call for verifying credentials + authority_route TEXT -- optional: route of the authority to call for verifying credentials +); + +-- This table stores policies. A policy is an expression which +-- can be evaluated against an environment (a list of name/value pairs) +-- to assess if a given action can be performed on a given resource +CREATE TABLE policy +( + resource TEXT NOT NULL, -- resource name + action TEXT NOT NULL, -- action name + expression BLOB NOT NULL -- encoded expression to evaluate +); + +CREATE UNIQUE INDEX policy_index ON policy (resource, action); + +--------- +-- NODES +--------- + +-- This table stores information about local nodes +CREATE TABLE node +( + name TEXT PRIMARY KEY, -- Node name + identifier TEXT NOT NULL, -- Identifier of the default identity associated to the node + verbosity INTEGER NOT NULL, -- Verbosity level used for logging + is_default INTEGER NOT NULL, -- boolean indicating if this node is the default one (0 means true) + is_authority INTEGER NOT NULL, -- boolean indicating if this node is an authority node (0 means true). This boolean is used to be able to show an authority node as UP even if its TCP listener cannot be accessed. + tcp_listener_address TEXT, -- Socket address for the node default TCP Listener (can be NULL if the node has not been started) + pid INTEGER -- Current process id of the node if it has been started +); + +-- This table stores the project name to use for a given node +CREATE TABLE node_project +( + node_name TEXT PRIMARY KEY, -- Node name + project_name TEXT NOT NULL -- Project name +); + +--------------------------- +-- PROJECTS, SPACES, USERS +--------------------------- + +-- This table store data about projects as returned by the Controller +CREATE TABLE project +( + project_id TEXT PRIMARY KEY, -- Identifier of the project + project_name TEXT NOT NULL, -- Name of the project + is_default INTEGER NOT NULL, -- boolean indicating if this project is the default one (0 means true) + space_id TEXT NOT NULL, -- Identifier of the space associated to the project + space_name TEXT NOT NULL, -- Name of the space associated to the project + identifier TEXT, -- optional: identifier of the project identity + access_route TEXT NOT NULL, -- Route used to create a secure channel to the project + authority_identity TEXT, -- Encoded identity of the authority associated to the project + authority_access_route TEXT, -- Route te the authority associated to the project + version TEXT, -- Orchestrator software version + running INTEGER, -- boolean indicating if this project is currently accessible + operation_id TEXT -- optional id of the operation currently creating the project on the Controller side +); + +-- This table provides the list of users associated to a given project +CREATE TABLE user_project +( + user_email TEXT NOT NULL, -- User email + project_id TEXT NOT NULL -- Project id +); + +-- This table provides additional information for users associated to a project or a space +CREATE TABLE user_role +( + user_id INTEGER NOT NULL, -- User id + project_id TEXT NOT NULL, -- Project id + user_email TEXT NOT NULL, -- User email + role TEXT NOT NULL, -- Role of the user: admin or member + scope TEXT NOT NULL -- Scope of the role: space, project, or service +); + +-- This table stores data about spaces as returned by the controller +CREATE TABLE space +( + space_id TEXT PRIMARY KEY, -- Identifier of the space + space_name TEXT NOT NULL, -- Name of the space + is_default INTEGER NOT NULL -- boolean indicating if this project is the default one (0 means true) +); + +-- This table provides the list of users associated to a given project +CREATE TABLE user_space +( + user_email TEXT NOT NULL, -- User email + space_id TEXT NOT NULL -- Space id +); + +-- This table provides additional information for users after they have been authenticated +CREATE TABLE user +( + email TEXT PRIMARY KEY, -- User email + sub TEXT NOT NULL, -- (Sub)ject: unique identifier for the user + nickname TEXT NOT NULL, -- User nickname (or handle) + name TEXT NOT NULL, -- User name + picture TEXT NOT NULL, -- Link to a user picture + updated_at TEXT NOT NULL, -- ISO-8601 date: when this user information was last update + email_verified INTEGER NOT NULL, -- boolean indicating if the user email has been verified (0 means true) + is_default INTEGER NOT NULL -- boolean indicating if this user is the default user locally (0 means true) +); + +--------------- +-- APPLICATION +--------------- + +-- This table stores the current state of an outlet created to expose a service with the desktop application +CREATE TABLE tcp_outlet_status +( + alias TEXT PRIMARY KEY, -- Name for the outlet + socket_addr TEXT NOT NULL, -- Socket address that the outlet connects to + worker_addr TEXT NOT NULL, -- Worker address for the outlet itself + payload TEXT -- Optional status payload +); + +-- This table stores the list of services that a user has been invited to connect to +-- via the desktop application +CREATE TABLE incoming_service +( + invitation_id TEXT PRIMARY KEY, -- Invitation id + enabled INTEGER NOT NULL, -- boolean indicating if the user wants to service to be accessible (0 means true) + name TEXT NULL -- Optional user-defined name for the service +); + +---------- +-- ADDONS +---------- + +-- This table stores the data necessary to configure the Okta addon +CREATE TABLE okta_config +( + project_id TEXT NOT NULL, -- Project id of the project using the addon + tenant_base_url TEXT NOT NULL, -- Base URL of the tenant + client_id TEXT NOT NULL, -- Client id + certificate TEXT NOT NULL, -- Certificate + attributes TEXT -- Comma-separated list of attribute names +); + +-- This table stores the data necessary to configure the Confluent addon +CREATE TABLE confluent_config +( + project_id TEXT NOT NULL, -- Project id of the project using the addon + bootstrap_server TEXT NOT NULL -- URL of the bootstrap server +); diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/mod.rs b/implementations/rust/ockam/ockam_node/src/storage/database/mod.rs new file mode 100644 index 00000000000..956af323eb3 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/mod.rs @@ -0,0 +1,5 @@ +mod sqlx_database; +mod sqlx_types; + +pub use sqlx_database::*; +pub use sqlx_types::*; diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs new file mode 100644 index 00000000000..685116f05a4 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs @@ -0,0 +1,216 @@ +use core::fmt::{Debug, Formatter}; +use sqlx::sqlite::SqliteConnectOptions; +use std::ops::Deref; +use std::path::Path; + +use ockam_core::errcode::{Kind, Origin}; +use sqlx::{ConnectOptions, SqlitePool}; +use tokio_retry::strategy::{jitter, FixedInterval}; +use tokio_retry::Retry; +use tracing::debug; +use tracing::log::LevelFilter; + +use ockam_core::compat::sync::Arc; +use ockam_core::{Error, Result}; + +/// We use sqlx as our primary interface for interacting with the database +/// The database driver is currently Sqlite +pub struct SqlxDatabase { + /// Pool of connections to the database + pub pool: SqlitePool, +} + +impl Debug for SqlxDatabase { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str(format!("database options {:?}", self.pool.connect_options()).as_str()) + } +} + +impl Deref for SqlxDatabase { + type Target = SqlitePool; + + fn deref(&self) -> &Self::Target { + &self.pool + } +} + +impl SqlxDatabase { + /// Constructor for a database persisted on disk + pub async fn create(path: impl AsRef) -> Result { + path.as_ref() + .parent() + .map(std::fs::create_dir_all) + .transpose() + .map_err(|e| Error::new(Origin::Api, Kind::Io, e.to_string()))?; + + // creating a new database might be failing a few times + // if the files are currently being held by another pod which is shutting down. + // In that case we retry a few times, between 1 and 10 seconds. + let retry_strategy = FixedInterval::from_millis(1000) + .map(jitter) // add jitter to delays + .take(10); // limit to 10 retries + + let db = Retry::spawn(retry_strategy, || async { + Self::create_at(path.as_ref()).await + }) + .await?; + db.migrate().await?; + Ok(db) + } + + /// Constructor for an in-memory database + /// The implementation blocks during the creation of the database + /// so that we don't have to propagate async in all the code base when using an + /// in-memory database, especially when writing examples + pub async fn in_memory(usage: &str) -> Result> { + debug!("create an in memory database for {usage}"); + let pool = Self::create_in_memory_connection_pool().await?; + let db = SqlxDatabase { pool }; + db.migrate().await?; + Ok(Arc::new(db)) + } + + async fn create_at(path: &Path) -> Result { + // Creates database file if it doesn't exist + let pool = Self::create_connection_pool(path).await?; + Ok(SqlxDatabase { pool }) + } + + async fn create_connection_pool(path: &Path) -> Result { + let options = SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true) + .log_statements(LevelFilter::Debug); + let pool = SqlitePool::connect_with(options) + .await + .map_err(Self::map_sql_err)?; + Ok(pool) + } + + async fn create_in_memory_connection_pool() -> Result { + let pool = SqlitePool::connect("sqlite::memory:") + .await + .map_err(Self::map_sql_err)?; + Ok(pool) + } + + async fn migrate(&self) -> Result<()> { + sqlx::migrate!("./src/storage/database/migrations") + .run(&self.pool) + .await + .map_err(Self::map_migrate_err) + } + + /// Map a sqlx error into an ockam error + pub fn map_sql_err(err: sqlx::Error) -> Error { + Error::new(Origin::Application, Kind::Io, err) + } + + /// Map a sqlx migration error into an ockam error + pub fn map_migrate_err(err: sqlx::migrate::MigrateError) -> Error { + Error::new( + Origin::Application, + Kind::Io, + format!("migration error {err}"), + ) + } + + /// Map a minicbor decode error into an ockam error + pub fn map_decode_err(err: minicbor::decode::Error) -> Error { + Error::new(Origin::Application, Kind::Io, err) + } +} + +/// This trait provides some syntax for transforming sqlx errors into ockam errors +pub trait FromSqlxError { + /// Make an ockam core Error + fn into_core(self) -> Result; +} + +impl FromSqlxError for core::result::Result { + fn into_core(self) -> Result { + self.map_err(|e| Error::new(Origin::Api, Kind::Internal, e.to_string())) + } +} + +/// This trait provides some syntax to shorten queries execution returning () +pub trait ToVoid { + /// Return a () value + fn void(self) -> Result<()>; +} + +impl ToVoid for core::result::Result { + fn void(self) -> Result<()> { + self.map(|_| ()).into_core() + } +} + +#[cfg(test)] +mod tests { + use sqlx::sqlite::SqliteQueryResult; + use sqlx::FromRow; + use tempfile::NamedTempFile; + + use crate::database::ToSqlxType; + + use super::*; + + /// This is a sanity check to test that the database can be created with a file path + /// and that migrations are running ok, at least for one table + #[tokio::test] + async fn test_create_identity_table() -> Result<()> { + let db_file = NamedTempFile::new().unwrap(); + let db = SqlxDatabase::create(db_file.path()).await?; + + let inserted = insert_identity(&db).await.unwrap(); + + assert_eq!(inserted.rows_affected(), 1); + Ok(()) + } + + /// This test checks that we can run a query and return an entity + #[tokio::test] + async fn test_query() -> Result<()> { + let db_file = NamedTempFile::new().unwrap(); + let db = SqlxDatabase::create(db_file.path()).await?; + + insert_identity(&db).await.unwrap(); + + // successful query + let result: Option = + sqlx::query_as("SELECT identifier FROM identity WHERE identifier=?1") + .bind("Ifa804b7fca12a19eed206ae180b5b576860ae651") + .fetch_optional(&db.pool) + .await + .unwrap(); + assert_eq!( + result, + Some(IdentifierRow( + "Ifa804b7fca12a19eed206ae180b5b576860ae651".into() + )) + ); + + // failed query + let result: Option = + sqlx::query_as("SELECT identifier FROM identity WHERE identifier=?1") + .bind("x") + .fetch_optional(&db.pool) + .await + .unwrap(); + assert_eq!(result, None); + Ok(()) + } + + /// HELPERS + async fn insert_identity(db: &SqlxDatabase) -> Result { + sqlx::query("INSERT INTO identity VALUES (?1, ?2)") + .bind("Ifa804b7fca12a19eed206ae180b5b576860ae651") + .bind("123".to_sql()) + .execute(&db.pool) + .await + .into_core() + } + + #[derive(FromRow, PartialEq, Eq, Debug)] + struct IdentifierRow(String); +} diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_types.rs b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_types.rs new file mode 100644 index 00000000000..c8080f6e2e1 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_types.rs @@ -0,0 +1,194 @@ +use std::net::SocketAddr; +use std::path::PathBuf; + +use ockam_core::Address; +use sqlx::database::HasArguments; +use sqlx::encode::IsNull; +use sqlx::{Database, Encode, Sqlite, Type}; +use time::OffsetDateTime; + +/// This enum represents the set of types that we currently support in our database +/// Since we support only Sqlite at the moment, those types are close to what is supported by Sqlite: +/// https://www.sqlite.org/datatype3.html +/// +/// The purpose of this type is to ease the serialization of data types in Ockam into data types in +/// our database. For example, if we describe how to translate an `Identifier` into some `Text` then +/// we can use the `Text` as a parameter in a sqlx query. +/// +/// Note: see the `ToSqlxType` trait and its instances for how the conversion is done +/// +pub enum SqlxType { + /// This type represents text in the database + Text(String), + /// This type represents arbitrary bytes in the database + Blob(Vec), + /// This type represents ints, signed or unsigned + Integer(i64), + /// This type represents floats + #[allow(unused)] + Real(f64), +} + +/// The SqlxType implements the Type trait from sqlx to allow its values to be serialized +/// to an Sqlite database +impl Type for SqlxType { + fn type_info() -> ::TypeInfo { + as Type>::type_info() + } +} + +/// The SqlType implements the Encode trait from sqlx to allow its values to be serialized +/// to an Sqlite database. There is a 1 to 1 mapping with the database native types +impl Encode<'_, Sqlite> for SqlxType { + fn encode_by_ref(&self, buf: &mut ::ArgumentBuffer) -> IsNull { + match self { + SqlxType::Text(v) => >::encode_by_ref(v, buf), + SqlxType::Blob(v) => as Encode<'_, Sqlite>>::encode_by_ref(v, buf), + SqlxType::Integer(v) => >::encode_by_ref(v, buf), + SqlxType::Real(v) => >::encode_by_ref(v, buf), + } + } + + fn produces(&self) -> Option<::TypeInfo> { + Some(match self { + SqlxType::Text(_) => >::type_info(), + SqlxType::Blob(_) => as Type>::type_info(), + SqlxType::Integer(_) => >::type_info(), + SqlxType::Real(_) => >::type_info(), + }) + } +} + +/// This trait can be implemented by any type that can be converted to a database type +/// Typically an `Identifier` (to a `Text`), a `TimestampInSeconds` (to an `Integer`) etc... +/// +/// This allows a value to be used as a bind parameters in a sqlx query for example: +/// +/// use std::str::FromStr; +/// use sqlx::query_as; +/// use ockam_node::database::{SqlxType, ToSqlxType}; +/// +/// // newtype for a UNIX-like timestamp +/// struct TimestampInSeconds(u64); +/// +/// // this implementation maps the TimestampInSecond type to one of the types that Sqlx +/// // can serialize for sqlite +/// impl ToSqlxType for TimestampInSeconds { +/// fn to_sql(&self) -> SqlxType { +/// self.0.to_sql() +/// } +/// } +/// +/// let timestamp = TimestampInSeconds(10000000); +/// let query = query_as("SELECT * FROM identity WHERE created_at >= $1").bind(timestamp.as_sql()); +/// +/// +pub trait ToSqlxType { + /// Return the appropriate sql type + fn to_sql(&self) -> SqlxType; +} + +impl ToSqlxType for String { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.clone()) + } +} + +impl ToSqlxType for &str { + fn to_sql(&self) -> SqlxType { + self.to_string().to_sql() + } +} + +impl ToSqlxType for bool { + fn to_sql(&self) -> SqlxType { + if *self { + 1.to_sql() + } else { + 0.to_sql() + } + } +} + +impl ToSqlxType for u64 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for u32 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for u16 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for u8 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for i32 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for i16 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for i8 { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(*self as i64) + } +} + +impl ToSqlxType for OffsetDateTime { + fn to_sql(&self) -> SqlxType { + SqlxType::Integer(self.unix_timestamp()) + } +} + +impl ToSqlxType for Vec { + fn to_sql(&self) -> SqlxType { + SqlxType::Blob(self.clone()) + } +} + +impl ToSqlxType for &[u8; 32] { + fn to_sql(&self) -> SqlxType { + SqlxType::Blob(self.to_vec().clone()) + } +} + +impl ToSqlxType for SocketAddr { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.to_string()) + } +} + +impl ToSqlxType for Address { + fn to_sql(&self) -> SqlxType { + SqlxType::Text(self.to_string()) + } +} + +impl ToSqlxType for PathBuf { + fn to_sql(&self) -> SqlxType { + SqlxType::Text( + self.as_path() + .to_str() + .unwrap_or("a path should be a valid string") + .into(), + ) + } +} diff --git a/implementations/rust/ockam/ockam_node/src/storage/file_key_value_storage.rs b/implementations/rust/ockam/ockam_node/src/storage/file_key_value_storage.rs deleted file mode 100644 index ff693e3d456..00000000000 --- a/implementations/rust/ockam/ockam_node/src/storage/file_key_value_storage.rs +++ /dev/null @@ -1,134 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::path::Path; - -use crate::ToStringKey; -use ockam_core::compat::boxed::Box; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::string::String; -use ockam_core::{async_trait, Result}; - -use crate::{FileValueStorage, InMemoryKeyValueStorage, KeyValueStorage, ValueStorage}; - -/// Key value storage backed by a file -/// An additional cache in used to access values in memory and avoid re-reading the file too -/// frequently -/// WARNING: This implementation provides limited consistency if the same file is reused from -/// multiple instances and/or processes. For example, if one process deletes a value, the other -/// process will still have it in its cache and return it on a Get query. -/// NOTE: Currently, unused -pub struct FileKeyValueStorage { - file_storage: FileValueStorage>, - cache: InMemoryKeyValueStorage, -} - -impl< - K: Serialize + for<'de> Deserialize<'de> + ToStringKey + Ord + Clone + Send + Sync + 'static, - V: Default + Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static, - > FileKeyValueStorage -{ - /// Create the file storage and in memory cache - pub async fn create(path: &Path) -> Result { - Ok(Self { - file_storage: FileValueStorage::create(path).await?, - cache: InMemoryKeyValueStorage::new(), - }) - } -} - -#[async_trait] -impl< - K: Serialize + for<'de> Deserialize<'de> + ToStringKey + Ord + Clone + Send + Sync + 'static, - V: Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static, - > KeyValueStorage for FileKeyValueStorage -{ - /// Put a value in the file storage and in cache for faster access - async fn put(&self, key: K, value: V) -> Result<()> { - let (k, v) = (key.clone(), value.clone()); - let f = move |mut map: BTreeMap| { - map.insert(key.to_string_key(), value.clone()); - Ok(map) - }; - self.file_storage.update_value(f).await?; - self.cache.put(k, v).await - } - - /// Get a value from cache. - /// If the value is not found in the cache try to find it in the file, then cache it - async fn get(&self, key: &K) -> Result> { - if let Some(value) = self.cache.get(key).await? { - Ok(Some(value)) - } else { - let k = key.to_string_key(); - let f = move |map: BTreeMap| Ok(map.get(&k).cloned()); - let retrieved_value = self.file_storage.read_value(f).await?; - if let Some(retrieved) = retrieved_value.clone() { - self.cache.put(key.clone(), retrieved).await?; - } - Ok(retrieved_value) - } - } - - /// Delete a value from the file and the cache - /// Return the value if it was found - async fn delete(&self, key: &K) -> Result> { - let k = key.to_string_key(); - let f = move |mut map: BTreeMap| { - let removed = map.remove(&k); - Ok((map, removed)) - }; - self.file_storage.modify_value(f).await?; - self.cache.delete(key).await - } - - /// Return the list of all the keys **in cache** - async fn keys(&self) -> Result> { - self.cache.keys().await - } -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use tempfile::NamedTempFile; - - use ockam_core::Result; - - #[tokio::test] - async fn test_file_key_value_storage() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let storage = FileKeyValueStorage::::create(file.path()).await?; - - // persist a new value - storage.put(Key::new(1, 2), Value(10)).await.unwrap(); - - // retrieve the value - let missing = storage.get(&Key::new(0, 0)).await?; - assert_eq!(missing, None); - - let updated = storage.get(&Key::new(1, 2)).await?; - assert_eq!(updated, Some(Value(10))); - - Ok(()) - } - - #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Debug, PartialOrd, Ord)] - struct Value(u8); - - #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Debug, PartialOrd, Ord)] - struct Key { - key1: u8, - key2: u8, - } - - impl ToStringKey for Key { - fn to_string_key(&self) -> String { - format!("{}_{}", self.key1, self.key2) - } - } - - impl Key { - fn new(key1: u8, key2: u8) -> Self { - Self { key1, key2 } - } - } -} diff --git a/implementations/rust/ockam/ockam_node/src/storage/file_value_storage.rs b/implementations/rust/ockam/ockam_node/src/storage/file_value_storage.rs deleted file mode 100644 index a8f020c70ff..00000000000 --- a/implementations/rust/ockam/ockam_node/src/storage/file_value_storage.rs +++ /dev/null @@ -1,287 +0,0 @@ -use crate::storage::value_storage::ValueStorage; -use crate::tokio::task::{self, JoinError}; -use cfg_if::cfg_if; -use fs2::FileExt; //locking -use ockam_core::compat::boxed::Box; -use ockam_core::errcode::{Kind, Origin}; -use ockam_core::{async_trait, Error, Result}; -use serde::{Deserialize, Serialize}; -use std::fs::File; -use std::io::BufReader; -use std::marker::PhantomData; -use std::path::{Path, PathBuf}; - -/// File Storage -/// There three files involved -/// - the actual file storing data -/// -/// - a temporary file used to avoid losing data during writes. The whole data file is copied to the -/// temporary file then the temporary file is renamed -/// -/// - a lock file. It's used to control inter-process accesses to the data. -/// Before reading or writing to the data fil, a shared or exclusive lock is first acquired -/// on this file. We don't lock over the data file directly, because doesn't play well with -/// the file rename we do -#[derive(Clone)] -pub struct FileValueStorage { - path: Box, - temp_path: Box, - lock_path: Box, - _phantom_data: PhantomData, -} - -impl Deserialize<'de>> FileValueStorage { - /// Create and init the file storage - pub async fn create(path: &Path) -> Result { - let mut s = Self::new(path); - s.init().await?; - Ok(s) - } - - /// Create the file storage but don't initialize it - fn new(path: &Path) -> Self { - let temp_path = Self::path_with_suffix(path, "tmp"); - let lock_path = Self::path_with_suffix(path, "lock"); - Self { - path: path.into(), - temp_path: temp_path.into(), - lock_path: lock_path.into(), - _phantom_data: PhantomData, - } - } - - /// Create FileStorage using file at given Path - /// If file doesn't exist, it will be created - async fn init(&mut self) -> Result<()> { - std::fs::create_dir_all(self.path.parent().unwrap()) - .map_err(|e| Error::new(Origin::Node, Kind::Io, e))?; - // This can block, but only when first initializing and just need to write an empty vault. - // So didn't bother to do it async - let lock_file = Self::open_lock_file(&self.lock_path)?; - lock_file - .lock_exclusive() - .map_err(|e| map_io_err(&self.lock_path, e))?; - - let should_flush_default = if self.path.exists() { - let metadata = self - .path - .metadata() - .map_err(|e| map_io_err(&self.path, e))?; - - metadata.len() == 0 - } else { - true - }; - - if should_flush_default { - let empty = V::default(); - Self::flush_to_file(&self.path, &self.temp_path, &empty)?; - } - lock_file - .unlock() - .map_err(|e| map_io_err(&self.lock_path, e))?; - Ok(()) - } - - fn load(path: &Path) -> Result { - let file = File::open(path).map_err(|e| map_io_err(path, e))?; - let reader = BufReader::new(file); - Ok(serde_json::from_reader::, V>(reader) - .map_err(|e| ValueStorageError::InvalidStorageData(e.to_string()))?) - } - - // Flush vault to target, using temp_path as intermediary file. - fn flush_to_file(target: &Path, temp_path: &Path, value: &V) -> Result<()> { - let data = serde_json::to_vec(value) - .map_err(|e| ValueStorageError::InvalidStorageData(e.to_string()))?; - use std::io::prelude::*; - cfg_if! { - if #[cfg(windows)] { - let mut file = std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(temp_path) - .map_err(|_| ValueStorageError::StorageError)?; - } else { - use std::os::unix::fs::OpenOptionsExt; - let mut file = std::fs::OpenOptions::new() - .write(true) - .create(true) - .mode(0o600) - .open(temp_path) - .map_err(|_| ValueStorageError::StorageError)?; - } - } - file.write_all(&data) - .map_err(|_| ValueStorageError::StorageError)?; - file.flush().map_err(|_| ValueStorageError::StorageError)?; - file.sync_all() - .map_err(|_| ValueStorageError::StorageError)?; - std::fs::rename(temp_path, target).map_err(|_| ValueStorageError::StorageError)?; - Ok(()) - } -} - -impl FileValueStorage { - fn path_with_suffix(path: &Path, suffix: &str) -> PathBuf { - match path.extension() { - None => path.with_extension(suffix), - Some(e) => path.with_extension(format!("{}.{}", e.to_str().unwrap(), suffix)), - } - } - - fn open_lock_file(lock_path: &Path) -> Result { - std::fs::OpenOptions::new() - .write(true) - .read(true) - .create(true) - .open(lock_path) - .map_err(|e| map_io_err(lock_path, e)) - } -} - -#[async_trait] -impl Deserialize<'a> + Serialize + Send + Sync + 'static> ValueStorage - for FileValueStorage -{ - async fn update_value(&self, f: impl Fn(V) -> Result + Send + Sync + 'static) -> Result<()> { - let f = move |v: V| Ok((f(v)?, ())); - let _ = self.modify_value(f).await?; - Ok(()) - } - - async fn modify_value( - &self, - f: impl Fn(V) -> Result<(V, R)> + Send + Sync + 'static, - ) -> Result { - let lock_path = self.lock_path.clone(); - let temp_path = self.temp_path.clone(); - let path = self.path.clone(); - let tr = move || -> Result { - let file = FileValueStorage::::open_lock_file(&lock_path)?; - file.lock_exclusive().map_err(|e| map_io_err(&path, e))?; - let existing_value = FileValueStorage::::load(&path)?; - let (updated_value, result) = f(existing_value)?; - FileValueStorage::::flush_to_file(&path, &temp_path, &updated_value)?; - // if something goes wrong it will be unlocked once the file handler get closed anyway - file.unlock().map_err(|e| map_io_err(&path, e))?; - Ok(result) - }; - task::spawn_blocking(tr).await.map_err(map_join_err)? - } - - async fn read_value( - &self, - f: impl Fn(V) -> Result + Send + Sync + 'static, - ) -> Result { - let path = self.path.clone(); - let lock_path = self.lock_path.clone(); - let tr = move || { - let file = FileValueStorage::::open_lock_file(&lock_path)?; - file.lock_shared().map_err(|e| map_io_err(&path, e))?; - let data = FileValueStorage::::load(&path)?; - let r = f(data)?; - // if something goes wrong it will be unlocked once the file handler get closed anyway - file.unlock().map_err(|e| map_io_err(&path, e))?; - Ok(r) - }; - task::spawn_blocking(tr).await.map_err(map_join_err)? - } -} - -fn map_join_err(err: JoinError) -> Error { - Error::new(Origin::Application, Kind::Io, err) -} - -fn map_io_err(path: &Path, err: std::io::Error) -> Error { - Error::new( - Origin::Application, - Kind::Io, - format!("{err} for path {:?}", path), - ) -} - -/// Represents the failures that can occur when storing values -#[derive(Clone, Debug)] -pub enum ValueStorageError { - /// IO error - StorageError, - /// Invalid Storage data - InvalidStorageData(String), -} - -impl ockam_core::compat::error::Error for ValueStorageError {} - -impl core::fmt::Display for ValueStorageError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::StorageError => write!(f, "invalid storage"), - Self::InvalidStorageData(e) => write!(f, "invalid storage data {:?}", e), - } - } -} - -impl From for Error { - #[track_caller] - fn from(err: ValueStorageError) -> Self { - Error::new(Origin::Vault, Kind::Invalid, err) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ockam_core::compat::rand::{thread_rng, Rng}; - use ockam_core::Result; - use tempfile::NamedTempFile; - - #[tokio::test] - async fn test_empty_file() -> Result<()> { - let path = NamedTempFile::new().unwrap(); - - let storage = FileValueStorage::::create(path.path()) - .await - .unwrap(); - - storage.update_value(Ok).await?; - - Ok(()) - } - - #[tokio::test] - async fn test_file_value_storage() -> Result<()> { - let file_name = hex::encode(thread_rng().gen::<[u8; 8]>()); - - let path = tempfile::tempdir() - .unwrap() - .into_path() - .with_file_name(file_name); - - let storage = FileValueStorage::::create(path.as_path()) - .await - .unwrap(); - - let initial = storage.read_value(Ok).await?; - - // sanity check - assert_eq!(Value::default(), Value(0)); - - // the initial value is the default value - assert_eq!(initial, Value::default()); - - // the value can be updated - storage - .update_value(move |_: Value| Ok(Value(10))) - .await - .unwrap(); - - // the new value can be read again - let updated = storage.read_value(Ok).await?; - assert_eq!(updated, Value(10)); - - Ok(()) - } - - #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug)] - struct Value(u8); -} diff --git a/implementations/rust/ockam/ockam_node/src/storage/in_memory_key_value_storage.rs b/implementations/rust/ockam/ockam_node/src/storage/in_memory_key_value_storage.rs deleted file mode 100644 index 425ae8c109d..00000000000 --- a/implementations/rust/ockam/ockam_node/src/storage/in_memory_key_value_storage.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::KeyValueStorage; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::{boxed::Box, sync::Arc, sync::RwLock, vec::Vec}; -use ockam_core::{async_trait, Result}; - -/// In memory implementation of a key / value storage -#[derive(Clone)] -pub struct InMemoryKeyValueStorage { - storage: Arc>>, -} - -impl Default for InMemoryKeyValueStorage { - fn default() -> Self { - InMemoryKeyValueStorage { - storage: Default::default(), - } - } -} - -#[async_trait] -impl KeyValueStorage - for InMemoryKeyValueStorage -{ - async fn put(&self, key: K, value: V) -> Result<()> { - let mut storage = self.storage.write().unwrap(); - storage.insert(key, value); - Ok(()) - } - - async fn get(&self, key: &K) -> Result> { - let storage = self.storage.read().unwrap(); - let value = storage.get(key).cloned(); - Ok(value) - } - - async fn delete(&self, key: &K) -> Result> { - let mut storage = self.storage.write().unwrap(); - Ok(storage.remove(key)) - } - - async fn keys(&self) -> Result> { - let storage = self.storage.read().unwrap(); - Ok(storage.keys().cloned().collect()) - } -} - -impl - InMemoryKeyValueStorage -{ - /// Create a new in-memory key / value storage - pub fn new() -> InMemoryKeyValueStorage { - InMemoryKeyValueStorage { - storage: Default::default(), - } - } - /// Create a new in-memory key / value storage - pub fn create() -> Arc> { - Arc::new(InMemoryKeyValueStorage::new()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ockam_core::Result; - use serde::{Deserialize, Serialize}; - - #[tokio::test] - async fn test_key_value_storage() -> Result<()> { - let storage = InMemoryKeyValueStorage::::create(); - - // a value can be inserted - storage.put(1, Value(10)).await.unwrap(); - - // the new value can be retrieved by key - let retrieved = storage.get(&0).await?; - assert_eq!(retrieved, None); - - let retrieved = storage.get(&1).await?; - assert_eq!(retrieved, Some(Value(10))); - - Ok(()) - } - - #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug, Clone)] - struct Value(u8); -} diff --git a/implementations/rust/ockam/ockam_node/src/storage/in_memory_value_storage.rs b/implementations/rust/ockam/ockam_node/src/storage/in_memory_value_storage.rs deleted file mode 100644 index 5ed38d631b7..00000000000 --- a/implementations/rust/ockam/ockam_node/src/storage/in_memory_value_storage.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::ValueStorage; -use ockam_core::compat::{boxed::Box, sync::Arc, sync::RwLock}; -use ockam_core::{async_trait, Result}; - -/// In memory implementation of a value storage -pub struct InMemoryValueStorage { - storage: Arc>, -} - -/// Trait implementation for ValueStorage -#[async_trait] -impl ValueStorage for InMemoryValueStorage { - async fn update_value(&self, f: impl Fn(V) -> Result + Send + Sync + 'static) -> Result<()> { - let _ = self - .modify_value(move |v| { - let updated = f(v)?; - Ok((updated.clone(), updated)) - }) - .await?; - Ok(()) - } - - async fn modify_value( - &self, - f: impl Fn(V) -> Result<(V, R)> + Send + Sync + 'static, - ) -> Result { - let mut value = self.storage.write().unwrap(); - let (updated, result) = f(value.clone())?; - *value = updated; - Ok(result) - } - - async fn read_value(&self, f: impl Fn(V) -> Result + Send + Sync + 'static) -> Result { - f(self.storage.read().unwrap().clone()) - } -} - -/// Default in-memory value storage, starting with a default value for V -impl InMemoryValueStorage { - /// Create a new in-memory value storage - pub fn create() -> InMemoryValueStorage { - InMemoryValueStorage { - storage: Default::default(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ockam_core::Result; - use serde::{Deserialize, Serialize}; - - #[tokio::test] - async fn test_value_storage() -> Result<()> { - let storage = InMemoryValueStorage::::create(); - - let initial = storage.read_value(Ok).await?; - - // sanity check - assert_eq!(Value::default(), Value(0)); - - // the initial value is the default value - assert_eq!(initial, Value::default()); - - // the value can be updated - storage - .update_value(move |_: Value| Ok(Value(10))) - .await - .unwrap(); - - // the new value can be read again - let updated = storage.read_value(Ok).await?; - assert_eq!(updated, Value(10)); - - Ok(()) - } - - #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug, Clone)] - struct Value(u8); -} diff --git a/implementations/rust/ockam/ockam_node/src/storage/key_value_storage.rs b/implementations/rust/ockam/ockam_node/src/storage/key_value_storage.rs deleted file mode 100644 index b6faf6d3bdd..00000000000 --- a/implementations/rust/ockam/ockam_node/src/storage/key_value_storage.rs +++ /dev/null @@ -1,18 +0,0 @@ -use ockam_core::compat::{boxed::Box, vec::Vec}; -use ockam_core::{async_trait, Result}; - -/// This trait defines a key/value storage -#[async_trait] -pub trait KeyValueStorage: Sync + Send + 'static { - /// Store a key / value - async fn put(&self, key: K, value: V) -> Result<()>; - - /// Retrieve a value. Return None if no value corresponds to the specified key - async fn get(&self, key: &K) -> Result>; - - /// Delete a value and return it if found - async fn delete(&self, key: &K) -> Result>; - - /// Return the list of all the keys - async fn keys(&self) -> Result>; -} diff --git a/implementations/rust/ockam/ockam_node/src/storage/mod.rs b/implementations/rust/ockam/ockam_node/src/storage/mod.rs index f1897523362..3b2412bfefd 100644 --- a/implementations/rust/ockam/ockam_node/src/storage/mod.rs +++ b/implementations/rust/ockam/ockam_node/src/storage/mod.rs @@ -1,32 +1,3 @@ -/// File implementation of a key value storage +/// Database support #[cfg(feature = "std")] -mod file_key_value_storage; - -/// File implementation of a value storage -#[cfg(feature = "std")] -mod file_value_storage; - -/// In memory implementation of a key value storage -mod in_memory_key_value_storage; - -/// In memory implementation of a value storage -mod in_memory_value_storage; - -/// Trait defining the functions for a key value storage -mod key_value_storage; - -/// This trait defines types which can be used as keys in JSON maps -mod to_string_key; - -/// Trait defining the functions for a value storage -mod value_storage; - -#[cfg(feature = "std")] -pub use file_key_value_storage::*; -#[cfg(feature = "std")] -pub use file_value_storage::*; -pub use in_memory_key_value_storage::*; -pub use in_memory_value_storage::*; -pub use key_value_storage::*; -pub use to_string_key::*; -pub use value_storage::*; +pub mod database; diff --git a/implementations/rust/ockam/ockam_node/src/storage/to_string_key.rs b/implementations/rust/ockam/ockam_node/src/storage/to_string_key.rs deleted file mode 100644 index 84701379ff1..00000000000 --- a/implementations/rust/ockam/ockam_node/src/storage/to_string_key.rs +++ /dev/null @@ -1,9 +0,0 @@ -use ockam_core::compat::string::String; - -/// This trait needs to be implemented by structs which are used as keys in a key/value file -/// This constraint is necessary due to the persistence of values as JSON in the underlying -/// FileValueStorage -pub trait ToStringKey { - /// Return a string representation to be used as a key in a JSON map - fn to_string_key(&self) -> String; -} diff --git a/implementations/rust/ockam/ockam_node/src/storage/value_storage.rs b/implementations/rust/ockam/ockam_node/src/storage/value_storage.rs deleted file mode 100644 index bb835c1bc78..00000000000 --- a/implementations/rust/ockam/ockam_node/src/storage/value_storage.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ockam_core::{async_trait, compat::boxed::Box, Result}; - -/// This trait defines a storage interface for serializable values -/// It uses a closures in its interface in order to support a transactional behaviour and recovery from -/// errors. If the closure does not return a successful value then no change is performed -/// -/// A ValueStorage is always supposed to contain a value of type V, which can then be read or modified -#[async_trait] -pub trait ValueStorage: Sync + Send + 'static { - /// Update the currently stored value - async fn update_value(&self, f: impl Fn(V) -> Result + Send + Sync + 'static) -> Result<()>; - - /// Update the currently stored value and return a result R - async fn modify_value( - &self, - f: impl Fn(V) -> Result<(V, R)> + Send + Sync + 'static, - ) -> Result; - - /// Read the currently stored value and either return the full value or a subset of it, as R - async fn read_value( - &self, - f: impl Fn(V) -> Result + Send + Sync + 'static, - ) -> Result; -} diff --git a/implementations/rust/ockam/ockam_transport_ble/examples/05-secure-channel-over-ble-transport-initiator.rs b/implementations/rust/ockam/ockam_transport_ble/examples/05-secure-channel-over-ble-transport-initiator.rs index 4532bc2a34a..177db500a00 100644 --- a/implementations/rust/ockam/ockam_transport_ble/examples/05-secure-channel-over-ble-transport-initiator.rs +++ b/implementations/rust/ockam/ockam_transport_ble/examples/05-secure-channel-over-ble-transport-initiator.rs @@ -3,7 +3,6 @@ use ockam_core::{route, Result}; use ockam_identity::{secure_channels, SecureChannelOptions}; use ockam_node::Context; - use ockam_transport_ble::driver::btleplug::BleAdapter; use ockam_transport_ble::driver::BleClient; use ockam_transport_ble::{BleTransport, BLE}; @@ -25,7 +24,7 @@ async fn async_main(mut ctx: Context) -> Result<()> { let ble = BleTransport::create(&ctx).await?; // Create an Entity to represent Alice. - let secure_channels = secure_channels(); + let secure_channels = secure_channels().await?; let alice = secure_channels .identities() .identities_creation() diff --git a/implementations/rust/ockam/ockam_vault/Cargo.toml b/implementations/rust/ockam/ockam_vault/Cargo.toml index 326b53e52a2..a1b1817052a 100644 --- a/implementations/rust/ockam/ockam_vault/Cargo.toml +++ b/implementations/rust/ockam/ockam_vault/Cargo.toml @@ -44,6 +44,7 @@ std = [ "tracing/std", "alloc", "p256/std", + "storage", ] # Feature: "no_std" enables functionality required for platforms @@ -68,7 +69,7 @@ alloc = [ "p256/pem", ] -storage = ["ockam_node", "ockam_node/storage", "std", "serde_cbor"] +storage = ["ockam_node/storage", "sqlx"] [dependencies] aes-gcm = { version = "0.9", default-features = false, features = ["aes"] } @@ -88,6 +89,7 @@ rand_pcg = { version = "0.3.1", default-features = false, optional = true } serde = { version = "1", default-features = false, features = ["derive"] } serde_cbor = { version = "0.11.2", optional = true } sha2 = { version = "0.10", default-features = false } +sqlx = { version = "0.7.3", optional = true } static_assertions = "1.1.0" thiserror = { version = "1.0.50", optional = true } tracing = { version = "0.1", default-features = false, features = ["attributes"] } diff --git a/implementations/rust/ockam/ockam_vault/src/lib.rs b/implementations/rust/ockam/ockam_vault/src/lib.rs index e605c85b64b..63f19ddb909 100644 --- a/implementations/rust/ockam/ockam_vault/src/lib.rs +++ b/implementations/rust/ockam/ockam_vault/src/lib.rs @@ -30,7 +30,6 @@ extern crate core; extern crate alloc; /// Storage -#[cfg(feature = "storage")] pub mod storage; /// Errors diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs index 97123ba2ffc..c84f8c4c151 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_secure_channels/vault_for_secure_channels.rs @@ -1,4 +1,16 @@ -use super::aes::make_aes; +use sha2::{Digest, Sha256}; + +use ockam_core::compat::boxed::Box; +use ockam_core::compat::collections::BTreeMap; +use ockam_core::compat::rand::{thread_rng, RngCore}; +use ockam_core::compat::sync::{Arc, RwLock}; +use ockam_core::compat::vec::{vec, Vec}; +use ockam_core::{async_trait, Result}; + +use crate::storage::SecretsRepository; + +#[cfg(feature = "storage")] +use crate::storage::SecretsSqlxDatabase; use crate::{ AeadSecret, AeadSecretKeyHandle, BufferSecret, HKDFNumberOfOutputs, HandleToSecret, HashOutput, @@ -7,39 +19,31 @@ use crate::{ AEAD_SECRET_LENGTH, }; -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::rand::{thread_rng, RngCore}; -use ockam_core::compat::sync::{Arc, RwLock}; -use ockam_core::compat::vec::{vec, Vec}; -use ockam_core::{async_trait, compat::boxed::Box, Result}; -use ockam_node::{InMemoryKeyValueStorage, KeyValueStorage}; - -use crate::legacy::{KeyId, StoredSecret}; -use sha2::{Digest, Sha256}; +use super::aes::make_aes; /// [`SecureChannelVault`] implementation using software pub struct SoftwareVaultForSecureChannels { ephemeral_buffer_secrets: Arc>>, ephemeral_aead_secrets: Arc>>, ephemeral_x25519_secrets: Arc>>, - // Use String as a key for backwards compatibility - static_x25519_secrets: Arc>, + static_x25519_secrets: Arc, } impl SoftwareVaultForSecureChannels { /// Constructor - pub fn new(storage: Arc>) -> Self { + pub fn new(repository: Arc) -> Self { Self { ephemeral_buffer_secrets: Default::default(), ephemeral_aead_secrets: Default::default(), ephemeral_x25519_secrets: Default::default(), - static_x25519_secrets: storage, + static_x25519_secrets: repository, } } - /// Create Software implementation Vault with [`InMemoryKeyVaultStorage`] - pub fn create() -> Arc { - Arc::new(Self::new(InMemoryKeyValueStorage::create())) + /// Create Software implementation Vault with an in-memory implementation to store secrets + #[cfg(feature = "storage")] + pub async fn create() -> Result> { + Ok(Arc::new(Self::new(SecretsSqlxDatabase::create().await?))) } } @@ -53,7 +57,7 @@ impl SoftwareVaultForSecureChannels { let handle = Self::compute_handle_for_public_key(&public_key); self.static_x25519_secrets - .put(hex::encode(handle.0.value()), secret.into()) + .store_x25519_secret(&handle, secret) .await?; Ok(handle) @@ -83,7 +87,11 @@ impl SoftwareVaultForSecureChannels { /// Return the total number of static x25519 secrets present in the Vault pub async fn number_of_static_x25519_secrets(&self) -> Result { - Ok(self.static_x25519_secrets.keys().await?.len()) + Ok(self + .static_x25519_secrets + .get_x25519_secret_handles() + .await? + .len()) } /// Return the total number of ephemeral x25519 secrets present in the Vault @@ -176,15 +184,10 @@ impl SoftwareVaultForSecureChannels { return Ok(secret.clone()); } - if let Some(stored_secret) = self - .static_x25519_secrets - .get(&hex::encode(handle.0.value())) + self.static_x25519_secrets + .get_x25519_secret(handle) .await? - { - return stored_secret.try_into(); - } - - Err(VaultError::KeyNotFound.into()) + .ok_or(VaultError::KeyNotFound.into()) } async fn get_buffer_secret(&self, handle: &SecretBufferHandle) -> Result { @@ -306,7 +309,7 @@ impl VaultForSecureChannels for SoftwareVaultForSecureChannels { ) -> Result { Ok(self .static_x25519_secrets - .delete(&hex::encode(secret_key_handle.0.value())) + .delete_x25519_secret(&secret_key_handle) .await? .is_some()) } diff --git a/implementations/rust/ockam/ockam_vault/src/software/vault_for_signing/vault_for_signing.rs b/implementations/rust/ockam/ockam_vault/src/software/vault_for_signing/vault_for_signing.rs index 96b684b317a..f271c2f5f3a 100644 --- a/implementations/rust/ockam/ockam_vault/src/software/vault_for_signing/vault_for_signing.rs +++ b/implementations/rust/ockam/ockam_vault/src/software/vault_for_signing/vault_for_signing.rs @@ -9,33 +9,32 @@ use crate::{ EDDSA_CURVE25519_SECRET_KEY_LENGTH, }; +use crate::storage::SecretsRepository; +#[cfg(feature = "storage")] +use crate::storage::SecretsSqlxDatabase; +use arrayref::array_ref; use ockam_core::compat::rand::thread_rng; use ockam_core::compat::sync::Arc; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{async_trait, compat::boxed::Box, Error, Result}; -use ockam_node::{InMemoryKeyValueStorage, KeyValueStorage}; - -use crate::legacy::KeyId; -use crate::software::legacy::StoredSecret; -use arrayref::array_ref; use sha2::{Digest, Sha256}; /// [`SigningVault`] implementation using software #[derive(Clone)] pub struct SoftwareVaultForSigning { - // Use String as a key for backwards compatibility - secrets: Arc>, + secrets: Arc, } impl SoftwareVaultForSigning { /// Constructor - pub fn new(secrets: Arc>) -> Self { + pub fn new(secrets: Arc) -> Self { Self { secrets } } - /// Create Software implementation Vault with [`InMemoryKeyVaultStorage`] - pub fn create() -> Arc { - Arc::new(Self::new(InMemoryKeyValueStorage::create())) + /// Create an in-memory Software implementation Vault + #[cfg(feature = "storage")] + pub async fn create() -> Result> { + Ok(Arc::new(Self::new(SecretsSqlxDatabase::create().await?))) } /// Import a key from a binary @@ -43,16 +42,14 @@ impl SoftwareVaultForSigning { let public_key = Self::compute_public_key_from_secret(&key)?; let handle = Self::compute_handle_for_public_key(&public_key)?; - self.secrets - .put(hex::encode(handle.handle().value()), key.into()) - .await?; + self.secrets.store_signing_secret(&handle, key).await?; Ok(handle) } /// Return the total number of keys pub async fn number_of_keys(&self) -> Result { - Ok(self.secrets.keys().await?.len()) + Ok(self.secrets.get_signing_secret_handles().await?.len()) } } @@ -139,9 +136,9 @@ impl VaultForSigning for SoftwareVaultForSigning { signing_secret_key_handle: SigningSecretKeyHandle, ) -> Result { self.secrets - .delete(&hex::encode(signing_secret_key_handle.handle().value())) + .delete_signing_secret(&signing_secret_key_handle) .await - .map(|r| r.is_some()) + .map(|s| s.is_some()) } } @@ -219,17 +216,12 @@ impl SoftwareVaultForSigning { &self, signing_secret_key_handle: &SigningSecretKeyHandle, ) -> Result { - let handle = match signing_secret_key_handle { - SigningSecretKeyHandle::EdDSACurve25519(handle) => handle, - SigningSecretKeyHandle::ECDSASHA256CurveP256(handle) => handle, - }; - let stored_secret = self .secrets - .get(&hex::encode(handle.value())) + .get_signing_secret(signing_secret_key_handle) .await? .ok_or(VaultError::KeyNotFound)?; - stored_secret.try_into() + Ok(stored_secret) } } diff --git a/implementations/rust/ockam/ockam_vault/src/storage/mod.rs b/implementations/rust/ockam/ockam_vault/src/storage/mod.rs index f33260668b8..47a38ceb019 100644 --- a/implementations/rust/ockam/ockam_vault/src/storage/mod.rs +++ b/implementations/rust/ockam/ockam_vault/src/storage/mod.rs @@ -1,4 +1,7 @@ -/// Storage of secrets to a file -mod persistent_storage; +mod secrets_repository; +#[cfg(feature = "storage")] +mod secrets_repository_sql; -pub use persistent_storage::*; +pub use secrets_repository::*; +#[cfg(feature = "storage")] +pub use secrets_repository_sql::*; diff --git a/implementations/rust/ockam/ockam_vault/src/storage/persistent_storage.rs b/implementations/rust/ockam/ockam_vault/src/storage/persistent_storage.rs deleted file mode 100644 index 2fb7e47581a..00000000000 --- a/implementations/rust/ockam/ockam_vault/src/storage/persistent_storage.rs +++ /dev/null @@ -1,187 +0,0 @@ -use ockam_core::compat::collections::BTreeMap; -use ockam_core::compat::sync::Arc; -use ockam_core::{async_trait, Result}; -use ockam_node::{FileValueStorage, InMemoryKeyValueStorage, KeyValueStorage, ValueStorage}; - -use crate::legacy::{KeyId, Secret, SecretAttributes, StoredSecret}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::path::Path; - -/// Storage for a Vault data backed by a file -/// The `FileValueStorage` implementation takes care of locking / unlocking the underlying file -/// in the presence of concurrent accesses -/// WARNING: This implementation provides limited consistency if the same file is reused from -/// multiple instances and/or processes. For example, if one process deletes a value, the other -/// process will still have it in its cache and return it on a Get query. -pub struct PersistentStorage { - storage: Arc>, - cache: InMemoryKeyValueStorage, -} - -impl PersistentStorage { - /// Create a new file storage for a Vault - pub async fn create(path: &Path) -> Result>> { - let storage = Arc::new(FileValueStorage::create(path).await?); - let cache = InMemoryKeyValueStorage::new(); - Ok(Arc::new(PersistentStorage { storage, cache })) - } -} - -/// This struct is serialized to a file in order to persist vault data -#[derive(Debug, Clone, Default)] -struct StoredSecrets { - secrets: BTreeMap, -} - -#[derive(Serialize, Deserialize)] -struct FileSecrets(Vec); - -#[derive(Serialize, Deserialize)] -struct FileSecret { - key_id: KeyId, - secret: Secret, - attributes: SecretAttributes, -} - -impl StoredSecrets { - fn add_stored_secret(&mut self, key_id: KeyId, stored_secret: StoredSecret) { - self.secrets.insert(key_id, stored_secret); - } - - fn get_stored_secret(&self, key_id: &KeyId) -> Option { - self.secrets.get(key_id).cloned() - } - - fn delete_stored_secret(&mut self, key_id: &KeyId) -> Option { - self.secrets.remove(key_id) - } -} - -impl Serialize for StoredSecrets { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut file_secrets = vec![]; - for (key_id, secret) in self.secrets.iter() { - file_secrets.push(FileSecret { - key_id: key_id.clone(), - secret: secret.secret().clone(), - attributes: secret.attributes(), - }); - } - FileSecrets(file_secrets).serialize(serializer) - } -} - -/// The deserialization for StoredSecrets needs to account for some changes in the stored data -/// - AWS keys are not stored anymore -/// - the persistence field for secrets is not necessary anymore -/// - the length of a secret is only needed for some secret types -impl<'de> Deserialize<'de> for StoredSecrets { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct StoredSecretsV2(FileSecrets); - - #[derive(Deserialize)] - #[serde(untagged)] - enum Secrets { - V2(StoredSecretsV2), - } - - match Secrets::deserialize(deserializer) { - Ok(Secrets::V2(StoredSecretsV2(file_secrets))) => { - let mut secrets: BTreeMap = Default::default(); - for secret in file_secrets.0 { - secrets.insert( - secret.key_id, - StoredSecret::new(secret.secret, secret.attributes), - ); - } - Ok(StoredSecrets { secrets }) - } - Err(e) => Err(e), - } - } -} - -/// A PersistentStorage can be seen as a key / value store -/// where we read the full data structure and put or get the wanted secret -#[async_trait] -impl KeyValueStorage for PersistentStorage { - async fn put(&self, key_id: KeyId, stored_secret: StoredSecret) -> Result<()> { - self.cache - .put(key_id.clone(), stored_secret.clone()) - .await?; - - let t = move |mut v: StoredSecrets| { - v.add_stored_secret(key_id.clone(), stored_secret.clone()); - Ok(v) - }; - self.storage.update_value(t).await - } - - async fn get(&self, key_id: &KeyId) -> Result> { - if let Ok(Some(s)) = self.cache.get(key_id).await { - return Ok(Some(s)); - } - let k = key_id.clone(); - let t = - move |v: StoredSecrets| -> Result> { Ok(v.get_stored_secret(&k)) }; - self.storage.read_value(t).await - } - - async fn delete(&self, key_id: &KeyId) -> Result> { - self.cache.delete(key_id).await?; - let k = key_id.clone(); - let t = move |mut v: StoredSecrets| -> Result<(StoredSecrets, Option)> { - let r = v.delete_stored_secret(&k); - Ok((v, r)) - }; - self.storage.modify_value(t).await - } - - /// Return the list of all the keys **in cache** - async fn keys(&self) -> Result> { - self.cache.keys().await - } -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use std::fs::File; - use std::io::Read; - use tempfile::NamedTempFile; - - #[tokio::test] - async fn test_persistent_storage() -> Result<()> { - let temp_file = NamedTempFile::new().unwrap(); - let storage = PersistentStorage::create(temp_file.path()).await?; - - // create and retrieve a persistent secret - let secret = Secret::new(vec![1; 32]); - let attributes = SecretAttributes::Ed25519; - let key_id = "34750f98bd59fcfc946da45aaabe933be154a4b5094e1c4abf42866505f3c97e".to_string(); - let stored_secret = StoredSecret::new(secret.clone(), attributes); - storage.put(key_id.clone(), stored_secret.clone()).await?; - - let mut file = File::open(temp_file.as_ref()).expect("Unable to open file"); - let mut file_contents = String::new(); - file.read_to_string(&mut file_contents) - .expect("Unable to read file"); - let expected = r#"[{"key_id":"34750f98bd59fcfc946da45aaabe933be154a4b5094e1c4abf42866505f3c97e","secret":"0101010101010101010101010101010101010101010101010101010101010101","attributes":"Ed25519"}]"#; - assert_eq!(file_contents, expected); - - let missing_key_id: KeyId = "missing-key-id".into(); - let actual = storage.get(&missing_key_id).await?; - assert_eq!(actual, None); - - let actual = storage.get(&key_id).await?; - assert_eq!(actual, Some(stored_secret)); - Ok(()) - } -} diff --git a/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository.rs b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository.rs new file mode 100644 index 00000000000..19a6c08d4c6 --- /dev/null +++ b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository.rs @@ -0,0 +1,53 @@ +use crate::{SigningSecret, SigningSecretKeyHandle, X25519SecretKey, X25519SecretKeyHandle}; +use ockam_core::async_trait; +use ockam_core::compat::boxed::Box; +use ockam_core::compat::vec::Vec; +use ockam_core::Result; + +/// A secrets repository supports the persistence of signing and X25519 secrets +#[async_trait] +pub trait SecretsRepository: Send + Sync + 'static { + /// Store a signing secret + async fn store_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + secret: SigningSecret, + ) -> Result<()>; + + /// Delete a signing secret + async fn delete_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + ) -> Result>; + + /// Get a signing secret + async fn get_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + ) -> Result>; + + /// Get the list of all signing secret handles + async fn get_signing_secret_handles(&self) -> Result>; + + /// Get a X25519 secret + async fn store_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + secret: X25519SecretKey, + ) -> Result<()>; + + /// Get a X25519 secret + async fn delete_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + ) -> Result>; + + /// Get a X25519 secret + async fn get_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + ) -> Result>; + + /// Get the list of all X25519 secret handles + async fn get_x25519_secret_handles(&self) -> Result>; +} diff --git a/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs new file mode 100644 index 00000000000..71c8de129db --- /dev/null +++ b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs @@ -0,0 +1,319 @@ +use sqlx::*; +use tracing::debug; + +use ockam_core::async_trait; +use ockam_core::compat::sync::Arc; +use ockam_core::compat::vec::Vec; +use ockam_core::errcode::{Kind, Origin}; +use ockam_core::Result; +use ockam_node::database::{FromSqlxError, SqlxDatabase, SqlxType, ToSqlxType, ToVoid}; + +use crate::storage::secrets_repository::SecretsRepository; + +use crate::{ + ECDSASHA256CurveP256SecretKey, EdDSACurve25519SecretKey, HandleToSecret, SigningSecret, + SigningSecretKeyHandle, X25519SecretKey, X25519SecretKeyHandle, +}; + +/// Implementation of a secrets repository using a SQL database +#[derive(Clone)] +pub struct SecretsSqlxDatabase { + database: Arc, +} + +impl SecretsSqlxDatabase { + /// Create a new database for policies keys + pub fn new(database: Arc) -> Self { + debug!("create a repository for secrets"); + Self { database } + } + + /// Create a new in-memory database for policies + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("secrets").await?, + ))) + } +} + +const ED_DSA_CURVE_25519: &str = "EdDSACurve25519"; +const EC_DSA_SHA256_CURVE_P256: &str = "ECDSASHA256CurveP256"; + +#[async_trait] +impl SecretsRepository for SecretsSqlxDatabase { + async fn store_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + secret: SigningSecret, + ) -> Result<()> { + let secret_type: String = match handle { + SigningSecretKeyHandle::EdDSACurve25519(_) => ED_DSA_CURVE_25519.into(), + SigningSecretKeyHandle::ECDSASHA256CurveP256(_) => EC_DSA_SHA256_CURVE_P256.into(), + }; + + let query = query("INSERT OR REPLACE INTO signing_secret VALUES (?, ?, ?)") + .bind(handle.to_sql()) + .bind(secret_type.to_sql()) + .bind(secret.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + ) -> Result> { + if let Some(secret) = self.get_signing_secret(handle).await? { + let query = query("DELETE FROM signing_secret WHERE handle = ?").bind(handle.to_sql()); + query.execute(&self.database.pool).await.void()?; + Ok(Some(secret)) + } else { + Ok(None) + } + } + + async fn get_signing_secret( + &self, + handle: &SigningSecretKeyHandle, + ) -> Result> { + let query = query_as("SELECT * FROM signing_secret WHERE handle=?").bind(handle.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.signing_secret()).transpose()?) + } + + async fn get_signing_secret_handles(&self) -> Result> { + let query = query_as("SELECT * FROM signing_secret"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + Ok(rows + .iter() + .map(|r| r.handle()) + .collect::>>()?) + } + + async fn store_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + secret: X25519SecretKey, + ) -> Result<()> { + let query = query("INSERT OR REPLACE INTO x25519_secret VALUES (?, ?)") + .bind(handle.to_sql()) + .bind(secret.to_sql()); + query.execute(&self.database.pool).await.void() + } + + async fn delete_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + ) -> Result> { + if let Some(secret) = self.get_x25519_secret(handle).await? { + let query = query("DELETE FROM x25519_secret WHERE handle = ?").bind(handle.to_sql()); + query.execute(&self.database.pool).await.void()?; + Ok(Some(secret)) + } else { + Ok(None) + } + } + + async fn get_x25519_secret( + &self, + handle: &X25519SecretKeyHandle, + ) -> Result> { + let query = query_as("SELECT * FROM x25519_secret WHERE handle=?").bind(handle.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(row.map(|r| r.x25519_secret()).transpose()?) + } + + async fn get_x25519_secret_handles(&self) -> Result> { + let query = query_as("SELECT * FROM x25519_secret"); + let rows: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + Ok(rows + .iter() + .map(|r| r.handle()) + .collect::>>()?) + } +} + +impl ToSqlxType for SigningSecret { + fn to_sql(&self) -> SqlxType { + match self { + SigningSecret::EdDSACurve25519(k) => k.key().to_sql(), + SigningSecret::ECDSASHA256CurveP256(k) => k.key().to_sql(), + } + } +} + +impl ToSqlxType for SigningSecretKeyHandle { + fn to_sql(&self) -> SqlxType { + self.handle().to_sql() + } +} + +impl ToSqlxType for X25519SecretKeyHandle { + fn to_sql(&self) -> SqlxType { + self.0.value().to_sql() + } +} + +impl ToSqlxType for HandleToSecret { + fn to_sql(&self) -> SqlxType { + self.value().to_sql() + } +} + +impl ToSqlxType for X25519SecretKey { + fn to_sql(&self) -> SqlxType { + self.key().to_sql() + } +} + +#[derive(FromRow)] +struct SigningSecretRow { + handle: Vec, + secret_type: String, + secret: Vec, +} + +impl SigningSecretRow { + fn signing_secret(&self) -> Result { + let secret: [u8; 32] = self.secret.clone().try_into().map_err(|_| { + ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + "cannot convert a signing secret to [u8; 32]", + ) + })?; + match self.secret_type.as_str() { + "EdDSACurve25519" => Ok(SigningSecret::EdDSACurve25519( + EdDSACurve25519SecretKey::new(secret), + )), + "ECDSASHA256CurveP256" => Ok(SigningSecret::ECDSASHA256CurveP256( + ECDSASHA256CurveP256SecretKey::new(secret), + )), + _ => Err(ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + "cannot deserialize a signing secret", + )), + } + } + + fn handle(&self) -> Result { + match self.secret_type.as_str() { + "EdDSACurve25519" => Ok(SigningSecretKeyHandle::EdDSACurve25519( + HandleToSecret::new(self.handle.clone()), + )), + "ECDSASHA256CurveP256" => Ok(SigningSecretKeyHandle::ECDSASHA256CurveP256( + HandleToSecret::new(self.handle.clone()), + )), + _ => Err(ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + "cannot deserialize a signing secret handle", + )), + } + } +} + +#[derive(FromRow)] +struct X25519SecretRow { + handle: Vec, + secret: Vec, +} + +impl X25519SecretRow { + fn x25519_secret(&self) -> Result { + let secret: [u8; 32] = self.secret.clone().try_into().map_err(|_| { + ockam_core::Error::new( + Origin::Api, + Kind::Serialization, + "cannot convert a X25519 secret to [u8; 32]", + ) + })?; + Ok(X25519SecretKey::new(secret)) + } + + fn handle(&self) -> Result { + Ok(X25519SecretKeyHandle(HandleToSecret::new( + self.handle.clone(), + ))) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn test_signing_secrets_repository() -> Result<()> { + let repository = create_repository().await?; + + let handle1 = + SigningSecretKeyHandle::ECDSASHA256CurveP256(HandleToSecret::new(vec![1, 2, 3])); + let secret1 = + SigningSecret::ECDSASHA256CurveP256(ECDSASHA256CurveP256SecretKey::new([1; 32])); + + let handle2 = SigningSecretKeyHandle::EdDSACurve25519(HandleToSecret::new(vec![4, 5, 6])); + let secret2 = SigningSecret::EdDSACurve25519(EdDSACurve25519SecretKey::new([1; 32])); + + repository + .store_signing_secret(&handle1, secret1.clone()) + .await?; + repository + .store_signing_secret(&handle2, secret2.clone()) + .await?; + + let result = repository.get_signing_secret(&handle1).await?; + assert!(result == Some(secret1)); + + let result = repository.get_signing_secret_handles().await?; + assert_eq!(result, vec![handle1.clone(), handle2]); + + repository.delete_signing_secret(&handle1).await?; + + let result = repository.get_signing_secret(&handle1).await?; + assert!(result.is_none()); + + Ok(()) + } + + #[tokio::test] + async fn test_x25519_secrets_repository() -> Result<()> { + let repository = create_repository().await?; + + let handle1 = X25519SecretKeyHandle(HandleToSecret::new(vec![1, 2, 3])); + let secret1 = X25519SecretKey::new([1; 32]); + + let handle2 = X25519SecretKeyHandle(HandleToSecret::new(vec![4, 5, 6])); + let secret2 = X25519SecretKey::new([1; 32]); + + repository + .store_x25519_secret(&handle1, secret1.clone()) + .await?; + repository + .store_x25519_secret(&handle2, secret2.clone()) + .await?; + + let result = repository.get_x25519_secret(&handle1).await?; + assert!(result == Some(secret1)); + + let result = repository.get_x25519_secret_handles().await?; + assert_eq!(result, vec![handle1.clone(), handle2]); + + repository.delete_x25519_secret(&handle1).await?; + + let result = repository.get_x25519_secret(&handle1).await?; + assert!(result.is_none()); + + Ok(()) + } + + /// HELPERS + async fn create_repository() -> Result> { + Ok(SecretsSqlxDatabase::create().await?) + } +}