diff options
| author | benj <benj@rse8.com> | 2022-12-01 10:30:34 -0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2022-12-01 10:35:50 -0800 |
| commit | 2c4eb2d311919ad9fb70738199ecf99bf20c9fce (patch) | |
| tree | 8739dd9d1d0c07fc27df2ece3d21f3a03db7397b | |
| parent | aa8c20d501b58001a5e1b24964c62363e2112ff8 (diff) | |
| download | secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.gz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.bz2 secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.lz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.xz secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.tar.zst secdiam-2c4eb2d311919ad9fb70738199ecf99bf20c9fce.zip | |
- basic functionality with psql and sqlite
- cli helper tool
51 files changed, 2908 insertions, 822 deletions
@@ -14,13 +14,10 @@ dependencies = [ ] [[package]] -name = "aho-corasick" -version = "0.7.19" +name = "anyhow" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" -dependencies = [ - "memchr", -] +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "async-attributes" @@ -34,11 +31,11 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ - "concurrent-queue 1.2.4", + "concurrent-queue", "event-listener", "futures-core", ] @@ -51,7 +48,7 @@ checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ "async-lock", "async-task", - "concurrent-queue 2.0.0", + "concurrent-queue", "fastrand", "futures-lite", "slab", @@ -74,13 +71,13 @@ dependencies = [ [[package]] name = "async-io" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" dependencies = [ "async-lock", "autocfg", - "concurrent-queue 1.2.4", + "concurrent-queue", "futures-lite", "libc", "log", @@ -89,7 +86,7 @@ dependencies = [ "slab", "socket2", "waker-fn", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -116,20 +113,20 @@ dependencies = [ [[package]] name = "async-process" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" +checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" dependencies = [ "async-io", + "async-lock", "autocfg", "blocking", "cfg-if", "event-listener", "futures-lite", "libc", - "once_cell", "signal-hook", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -168,9 +165,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -178,12 +175,6 @@ dependencies = [ ] [[package]] -name = "async_once" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce4f10ea3abcd6617873bae9f91d1c5332b4a778bd9ce34d0cd517474c1de82" - -[[package]] name = "atoi" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -204,7 +195,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -238,16 +229,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", + "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", - "once_cell", ] [[package]] @@ -264,21 +255,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" - -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cc" -version = "1.0.76" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -287,12 +272,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "concurrent-queue" -version = "1.2.4" +name = "clap" +version = "4.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" dependencies = [ - "cache-padded", + "os_str_bytes", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", ] [[package]] @@ -352,9 +376,9 @@ checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" [[package]] name = "crossbeam-queue" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if", "crossbeam-utils", @@ -362,9 +386,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if", ] @@ -404,9 +428,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -455,16 +479,24 @@ dependencies = [ ] [[package]] -name = "env_logger" -version = "0.9.3" +name = "errno" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", ] [[package]] @@ -640,9 +672,9 @@ dependencies = [ [[package]] name = "gloo-timers" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +checksum = "98c4a8d6391675c6b2ee1a6c8d06e8e2d03605c44cec1270675985a4c2a5500b" dependencies = [ "futures-channel", "futures-core", @@ -687,6 +719,15 @@ dependencies = [ ] [[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -711,10 +752,34 @@ dependencies = [ ] [[package]] -name = "humantime" -version = "2.1.0" +name = "home" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408" +dependencies = [ + "winapi", +] + +[[package]] +name = "iam" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "clap", + "colored", + "home", + "log", + "rand", + "secd", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", + "toml", + "uuid", +] [[package]] name = "idna" @@ -728,9 +793,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -746,6 +811,28 @@ dependencies = [ ] [[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes", + "rustix", + "windows-sys 0.42.0", +] + +[[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -802,6 +889,12 @@ dependencies = [ ] [[package]] +name = "linux-raw-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" + +[[package]] name = "lock_api" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -887,9 +980,9 @@ checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "openssl" -version = "0.10.42" +version = "0.10.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" +checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376" dependencies = [ "bitflags", "cfg-if", @@ -919,9 +1012,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.77" +version = "0.9.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a" +checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132" dependencies = [ "autocfg", "cc", @@ -931,6 +1024,12 @@ dependencies = [ ] [[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] name = "parking" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1013,16 +1112,16 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "polling" -version = "2.4.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" +checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" dependencies = [ "autocfg", "cfg-if", "libc", "log", "wepoll-ffi", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -1032,6 +1131,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] name = "proc-macro2" version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1100,23 +1223,6 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] name = "remove_dir_all" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1135,6 +1241,26 @@ dependencies = [ ] [[package]] +name = "rustix" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.42.0", +] + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + +[[package]] name = "ryu" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1147,7 +1273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -1162,10 +1288,9 @@ version = "0.1.0" dependencies = [ "async-std", "async-trait", - "async_once", + "base64", "derive_more", "email_address", - "env_logger", "lazy_static", "log", "openssl", @@ -1173,6 +1298,8 @@ dependencies = [ "serde", "serde_json", "sqlx", + "strum", + "strum_macros", "thiserror", "time", "uuid", @@ -1209,18 +1336,18 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" dependencies = [ "proc-macro2", "quote", @@ -1229,9 +1356,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -1429,6 +1556,31 @@ dependencies = [ ] [[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1436,9 +1588,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" dependencies = [ "proc-macro2", "quote", @@ -1531,6 +1683,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1582,11 +1743,12 @@ dependencies = [ [[package]] name = "uuid" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ "getrandom", + "serde", ] [[package]] @@ -1756,39 +1918,96 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] [[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" @@ -1,24 +1,9 @@ -[package] -name = "secd" -version = "0.1.0" -edition = "2021" +[workspace] +members = [ + "crates/iam", + "crates/secd", +] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -async_once = { version = "0.2.6" } -async-std = { version = "1", features = [ "attributes" ] } -async-trait = { version = "0.1" } -derive_more = { version = "0.99" } -email_address = { version = "0.2" } -env_logger = { version = "0.9" } -lazy_static = { version = "1.4" } -log = { version = "0.4" } -openssl = { version = "0.10.42" } -rand = { version = "0.8" } -serde = { version = "1" } -serde_json = { version = "1.0", features = ["raw_value"] } -sqlx = { version = "0.6", features = [ "runtime-async-std-native-tls", "postgres", "uuid", "sqlite", "time" ] } -time = { version = "0.3", features = [ "serde" ] } -thiserror = { version = "1.0" } -uuid = { version = "1.2", features = ["v4"]}
\ No newline at end of file +[profile.release] +strip = true +lto = true
\ No newline at end of file @@ -1,6 +1,19 @@ -# SecD +# SecD(iam) -Don't keep using expensive auth providers. secd provides everything you need to hook up your web service with the most common auth providers and mechanisms. Use secd with your language of choice. You can also use the Terraform template to launch secd a service within your cloud environment so you really don't ever need to worry about auth again. +Identity and Access Management enabled by the Sec(urity)D(aemon) but contained in your own infrastructure. + +Don't keep using expensive auth providers. secD(iam) is a library which provides robust authentication and custom authorization. You can use one of our pre-configured servers behind a proxy, embed the library into your own application, or build a custom authorization and authentication solution. + +secD(iam) allows you to build web, native, mobile, embedded, or any other application for which you want to leverage some type of identity and access management paradigm. Rather than lock yourself into vendors that make it impossible to easily query, manipulate, or otherwise interact with your data, use secD(iam) to easily enable authentication and authorization without hassle. + +Get started with: +- the secD binary +- a preconfigured secD server +- browsing secD integration for your Flask, Rails, Django, Laravel, React, Svelte, Angular, Micronaut, FastAPI, Spring Boot project +- the secD library for Python, Go, Rust, Ruby, Java, Javascript, C, C#, PHP +- the Terraform template to launch a secD proxy server in AWS, Google Cloud, or Azure. + +You can also use the Terraform template to launch secd a service within your cloud environment so you really don't ever need to worry about auth again. ## Quick Start @@ -14,11 +27,13 @@ secd provides a simple way for you to authenticate and authorize users while mai **Authorization** is the process of validating that something is allowed to do the thing it wants to do. -secd is a comprehensive, simple, pluggable API that provides the following high level functionality. +secd is a comprehensive, simple, pluggable API that provides the following high level functionality: + + ### Authentication -Register a new **identity** which is an opaque handle pointing to anything in the service. You can save this handle next to user/device information. +Register a new **identity** which is an opaque handle. You can save this handle next to your user/device information. Save a new **authentication credential** for an **identity**. This is a secret of some sort that allows an identity to prove it is what it says it is. @@ -36,9 +51,32 @@ Create a new authentication credential in multiple ways by **initiating** an **a **revoke** an identity's authentication. This revokes all valid credentials associated with an identity. Alternatively, only revoke a token, all tokens, a specific API key or all API keys. - - - ### Authorization tbd... ALlow + +## iam cli + +``` +iam create validation --email benjaminbellon@gmail.com +iam create session --validation-id 1234 --code 1234 +iam create group my-group --identity 1 2 3 4 +iam create role my-role --permission 1 2 3 4 +iam create service my-service --uri a/b/c +iam create permission my-permission --service my-service +iam ls group +iam ls role --filter re*g$ex +iam link group my-group identity 1 2 3 +iam link role my-role permission perm1 perm2 perm3 +iam get identity 123 +iam get group my-group +``` + +## Roadmap +- JWT authentication +- Oauth providers to add +- Capabilities scheme + +### Who Are We + +An enthusiastic collection of application developers, cryptography professionals, security tinfoil-hat-wearing users, and embedded developers with experience across hardware and software from small startups through AWS, Google, Apple, and beyond. diff --git a/crates/iam/Cargo.toml b/crates/iam/Cargo.toml new file mode 100644 index 0000000..fd9006d --- /dev/null +++ b/crates/iam/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "iam" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +async-std = { version = "1.12.0", features = [ "attributes" ] } +clap = { version = "4.0.29", features = ["derive"] } +colored = "2.0.0" +home = "0.5.4" +log = "0.4" +rand = "0.8" +secd = { path = "../secd" } +serde = "1" +serde_json = { version = "1.0", features = ["raw_value"] } +strum = "0.24.1" +strum_macros = "0.24" +toml = "0.5.9" +thiserror = "1.0" +uuid = { version = "1.2", features = ["v4", "serde"]}
\ No newline at end of file diff --git a/crates/iam/src/api.rs b/crates/iam/src/api.rs new file mode 100644 index 0000000..5819533 --- /dev/null +++ b/crates/iam/src/api.rs @@ -0,0 +1,498 @@ +use crate::ISSUE_TRACKER_LOC; +use clap::{Parser, Subcommand, ValueEnum}; +use colored::*; +use serde::{Deserialize, Serialize}; +use thiserror; +use uuid::Uuid; + +#[derive(Debug, thiserror::Error)] +pub enum CliError { + #[error("{}", "iam failed to read a valid configuration profile. Initialize an iam store with `iam admin init`".red())] + InvalidProfile, + #[error("{} {}", "Failed to initialize secd: ".red(), .0.yellow())] + SecdInitializationFailure(String), + #[error("{} {}", "Fail to initialize an iam store.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC))] + AdminInitializationError, + #[error("{} {}", "The provided validation id and code is invalid or has expired.".red(), "You may recieve at most one session with a valid code, after which a new validation is required.")] + InvalidCode, + #[error("{} {}", "An unknown error occurred.".red(), format!("An invariant was likely broken and should be reported as a bug here: {}", ISSUE_TRACKER_LOC))] + Unknown, +} + +#[derive(Parser)] +#[command( + name = "iam", + author = "benabel", + version = "0.0.1", + long_about = "SecD IAM\nIdentity and access management secured by secd and user controlled. Get started with `iam init`" +)] +pub struct Args { + #[command(subcommand)] + pub command: Command, + /// IAM backend profile as defined in the iam config or as a connection string to an auth store. + #[arg(long, short)] + pub profile: Option<String>, + /// The store type for the associated connection string + #[arg(long, short)] + pub store_type: Option<StoreType>, + /// Connection string for the IAM store + #[arg(long, short)] + pub connection_string: Option<String>, +} + +#[derive(Clone, ValueEnum)] +pub enum StoreType { + Dynamo, + Memory, + Mysql, + Postgres, + Redis, + Sqlite, +} + +#[derive(Subcommand)] +pub enum Command { + #[command( + about = "Administrative actions for this IAM instance", + long_about = "Admin\n\nAdministrative actions for this IAM instance. Each IAM instance is defined by the specified backend in the iam config or manually as an optional argument" + )] + Admin { + #[command(subcommand)] + action: AdminAction, + }, + #[command( + about = "Create a new IAM object which may be used to enforce authorization schemes.", + long_about = "Create\n\nEntities which define the structure of an IAM instance. These entities may be rendered as an IAM graph, or within a web view, to more easily visualize and manipulate the IAM instance." + )] + Create { + #[command(subcommand)] + object: CreateObject, + }, + #[command( + about = "Get details for a specific IAM object", + long_about = "Get\n\nGet details for a specific IAM object" + )] + Get { + #[command(subcommand)] + object: GetObject, + }, + #[command( + about = "Initialize an IAM store (alias for `iam admin init`)", + long_about = "Init\n\nInitalize a new IAM admin store and save the store's configuration profile. This command is an alias for, and thus equiavlent to, `iam admin init`." + )] + Init { + /// If true, interactively initialize an IAM store. Otherwise output a template config. + #[arg(long, short, action)] + interactive: bool, + }, + #[command( + about = "Link multiple IAM objects together", + long_about = "Link\n\nCleave different IAM entities to create an IAM system." + )] + Link { + #[command(subcommand)] + object: LinkObject, + /// Unlink the provided entities rather than link them. + #[arg(long, short, action)] + unlink: bool, + }, + #[command( + about = "List and filter IAM objects", + long_about = "List\n\nPage through collections of IAM objects with optional filtering" + )] + Ls { + #[command(subcommand)] + object: ListObject, + /// Regex filter for entity names + #[arg(long, short)] + name: Option<String>, + /// Only fetch entities created after this time + #[arg(long, short)] + after: Option<i64>, + /// Only fetch entities created before this time + #[arg(long, short)] + before: Option<i64>, + }, + /// Start the iam repl to more easily interact with iam and its primitives + Repl, +} + +#[derive(Subcommand)] +pub enum AdminAction { + /// Aliased as `iam init` + Init { + /// If true, interactively initialize an IAM store. Otherwise output a template config. + #[arg(long, short, action)] + interactive: bool, + }, + /// Configure, describe, or rotate the default IAM store. + Backend { + #[command(subcommand)] + action: AdminBackendAction, + }, + /// Create a new administrative entity for an IAM store. + Create { + #[command(subcommand)] + object: AdminObject, + }, + /// Seal the configured IAM store to prevent administrative changes + Seal, + /// Unseal the configured IAM store to make administrative changes + Unseal { + /// The secret key used to seal this store + secret_key: String, + }, +} + +#[derive(Subcommand)] +pub enum AdminBackendAction { + Configure { + name: String, + store: StoreType, + connection: String, + }, + Switch { + name: String, + }, +} + +#[derive(Subcommand)] +pub enum AdminObject { + /// An email template used for IAM procedures, including identity validation + EmailTemplate { + template_type: EmailTemplateType, + template: String, + }, + /// A selected provider capable of sending email messages + EmailProvider { + provider: EmailProvider, + secret_key: String, + public_key: Option<String>, + }, + /// A selected Oauth2.0 provider capable of authenticating identities + OauthProvider { + provider: OauthProvider, + client_id: String, + secret: String, + redirect_uri: String, + }, + /// A selected provider capable of sending SMS + SmsProvider { + provider: SmsProvider, + secret_key: String, + public_key: Option<String>, + }, + /// A new secret which may be used to unseal the IAM store + StoreSecret, + /// A selected provider capable of sending automated voice messages + VoiceProvider { + provider: VoiceProvider, + secret_key: String, + public_key: Option<String>, + }, +} + +#[derive(Clone, ValueEnum)] +pub enum EmailTemplateType { + Login, + SignUp, +} + +#[derive(Clone, ValueEnum)] +pub enum EmailProvider { + Custom, + Mailgun, + Sendgrid, + Ses, +} + +#[derive(Clone, ValueEnum)] +pub enum SmsProvider { + AwsSns, + Custom, + Twilio, +} + +#[derive(Clone, ValueEnum)] +pub enum VoiceProvider { + Custom, + Twilio, +} + +#[derive(Subcommand)] +pub enum CreateObject { + #[command( + about = "A set of long-lived tokens which authorize an identity", + long_about = "Api Keys\n\nApi keys are long lived identifiers which authenticate and authorize a identity. Keys have a public and private part,\nwhich may be shared and must be kept private, respectively. Unlike sessions, api keys may be long-lived (infinite) or\nset to expire within certain timeframes." + )] + ApiKey { + /// Identity against which this api key will be linked + identity: Uuid, + /// Time this api key expires (epoch time) + expires_at: Option<i64>, + }, + #[command( + about = "A collection of identities", + long_about = "Group\n\nA group may be created to operate simultaneously against a collection of identities. An identity may be part of mutliple groups, but it may not be part of the same group more than once." + )] + Group { + /// The unique name for this group + name: String, + /// An optional set of identities to link against this group + identities: Vec<Uuid>, + }, + #[command( + about = "A collection of services and service actions", + long_about = "Permission\n\nA permission may be created to operate simultaneously against a collection of services and service actions. A service or service action may be part of mutliple permissions, but it may not be part of the same permission more than once. A permission may be used when many services and service actions are linked and unlinked against a role." + )] + Permission { + /// An optional set of services to link against this permission + #[arg(long, short)] + services: Vec<Uuid>, + /// An optional set of service actions to link against this permission + #[arg(long, short)] + actions: Vec<Uuid>, + }, + #[command( + about = "A collection of permissions", + long_about = "Role\n\nA role may be created to operate simultaneously against a collection of permissions. A permission may be part of mutliple roles, but it may not be part of the same role more than once. A role may be used when many entities (such as groups or identities) are linked and unlinked against many permissions." + )] + Role { + /// The unique name for this role + name: String, + /// An optional set of permissions to link against this role + permissions: Vec<Uuid>, + }, + #[command( + about = "An entity for which an action may be authorized", + long_about = "Service\n\nA service is an atomic entity which requires authorization. While a service's authorization may be subdivided by service actions, a service represents a logical element of authorization separation." + )] + Service { + /// The unique name for this service + name: String, + /// URI for this service which may be used to resolve authorization + #[arg(long, short)] + uri: Option<String>, + }, + #[command( + about = "A specific authorization action by a service", + long_about = "Service Action\n\nA service action is a domain specific action which defines what an identity authorization within that service. A service action may be a simple boolean value or a more complex express which is evaluated at runtime. For example, a boolean action may be something like `can_read_salary_table`, and a more complex action may be `readable_table_rows(datetime)` which executes at runtime and returns a value (or list of values) the service may use to determine authorization. Service actions are used as an inversion of control pattern to ensure that services do not need to worry about specific authorization actions for identities. A service action is unnecessary if the service has no specific authorization logic." + )] + ServiceAction { + /// The unique name for this service action + name: String, + /// Program executed for this service action + #[arg(long, short)] + program: Option<String>, + }, + #[command( + about = "A timebound token which authorizes an identity", + long_about = "Session\n\nA session is an opaque timebound token which allows an identity to authorize against IAM services. The session may be created by providing a validation request id and secret challenge code" + )] + Session { + /// The validation id associated with a non-expired valid validation + #[arg(long, short)] + validation_id: Uuid, + /// The secret code associated with this validation. + #[arg(long, short)] + secret_code: String, + }, + #[command( + about = "An action which initiates an identity validation", + long_about = "Validation\n\nA validation requires that the identity authenticate in some way, either by providing IAM managed credentials, an external gated mechanism (e.g. email, phone, or hardware key), or through a secondary authentication provider (oauth, saml, ldap, kerberos)." + )] + Validation { + /// Method by which the validation will occur + #[command(subcommand)] + method: ValidationMethod, + /// The identity against which to associate this validation. A new identity will be created if no identity is provided. + #[arg(long, short)] + identity: Option<Uuid>, + }, +} + +#[derive(Subcommand)] +pub enum ValidationMethod { + /// An email address to which the validation will be sent + Email { + /// Email address which will receive the validation + address: String, + }, + /// A hardware security key to associate with an identity + HardwareKey, + /// A kerberos ticket to associated with an identity + Kerberos, + /// An oauth2 provider to authenticate (and authorize) an identity + Oauth2 { + provider: OauthProvider, + /// An optional scope to use for authorization + scope: Option<String>, + }, + /// A phone which an identity may authenticate via SMS or voice + Phone { + /// Whether to use a voice code. Otherwise, uses SMS + #[arg(long, short, action)] + use_voice: bool, + }, + /// A saml provider to authenticate an identity + Saml, +} + +#[derive(Clone, ValueEnum)] +pub enum OauthProvider { + Amazon, + Apple, + Dropbox, + Facebook, + Github, + Gitlab, + Google, + Instagram, + LinkedIn, + Microsoft, + Paypal, + Reddit, + Spotify, + Strava, + Stripe, + Twitch, + Twitter, + WeChat, +} + +#[derive(Subcommand)] +pub enum GetObject { + ApiKey { + /// Public key associated with this api key set + public_key: String, + }, + Group { + /// Unique group name + name: String, + /// Unique group id + #[arg(long, short)] + id: Option<Uuid>, + }, + Identity { + /// Unique identity id + id: Uuid, + }, + Permission { + /// Unique permission name + name: String, + /// Unique permission id + #[arg(long, short)] + id: Option<Uuid>, + }, + Role { + /// Unique role name + name: String, + /// Unique role id + #[arg(long, short)] + id: Option<Uuid>, + }, + Session { + /// The plaintext token which uniquely identifies the session + secret: String, + }, + Service { + /// Unique service name + name: String, + /// Unique service id + #[arg(long, short)] + id: Option<Uuid>, + }, + ServiceAction { + /// Unique service action name + name: String, + /// Unique service action id + #[arg(long, short)] + id: Option<Uuid>, + }, + Validation { + /// Unique validation request id + id: Uuid, + }, +} + +#[derive(Subcommand)] +pub enum LinkObject { + Group { + group_name: String, + #[arg(short, long, alias = "id")] + group_id: Option<Uuid>, + + identity_ids: Vec<Uuid>, + }, + Identity { + identity_id: Uuid, + + group_names: Vec<String>, + #[arg(long)] + group_ids: Vec<Uuid>, + }, + Permission { + permission_name: String, + #[arg(short, long, alias = "id")] + permission_id: Option<Uuid>, + + role_names: Vec<String>, + #[arg(long)] + role_ids: Vec<Uuid>, + }, + Role { + role_name: String, + #[arg(short, long, alias = "id")] + role_id: Option<Uuid>, + + permission_names: Vec<String>, + #[arg(long)] + permission_ids: Vec<Uuid>, + }, + Service { + service_name: String, + #[arg(short, long, alias = "id")] + service_id: Option<Uuid>, + + permission_names: Vec<String>, + #[arg(long)] + permission_ids: Vec<Uuid>, + }, + ServiceAction { + service_action_name: String, + #[arg(short, long, alias = "id")] + service_action_id: Option<Uuid>, + + service_name: Vec<String>, + #[arg(long)] + service_ids: Vec<Uuid>, + }, +} + +#[derive(Subcommand)] +pub enum ListObject { + ApiKey, + Group, + Identity, + Permission, + Role, + Session, + Service, + ServiceAction, + Validation, +} + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub profile: Vec<ConfigProfile>, +} + +#[derive(Serialize, Deserialize)] +pub struct ConfigProfile { + pub name: String, + pub store: secd::AuthStore, + pub store_conn: String, + pub emailer: secd::AuthEmail, + pub email_template_login: Option<String>, + pub email_template_signup: Option<String>, +} diff --git a/crates/iam/src/command.rs b/crates/iam/src/command.rs new file mode 100644 index 0000000..e9e0f23 --- /dev/null +++ b/crates/iam/src/command.rs @@ -0,0 +1,164 @@ +use crate::{ + api, + util::{self, get_config_profile, Result}, + CONFIG_LOGIN_TEMPLATE, CONFIG_SIGNUP_TEMPLATE, +}; +use async_std::fs; +use colored::*; +use rand::distributions::{Alphanumeric, DistString}; +use secd::{AuthEmail, AuthStore}; +use std::{ + fs::File, + io::{self, stdin, stdout, Write}, + str::FromStr, +}; +use strum::VariantNames; + +const DEFAULT_LOGIN_EMAIL: &str = "<!doctype html><html><body><p>You requested a login link for %secd_email_address%. Please click the following link<br/><br/>http://localhost:5500/myapp/iam/exchange/%secd_link%<br/><br/>or use code: %secd_code%</p></body></html>"; +const DEFAULT_SIGNUP_EMAIL: &str = "<!doctype html><html><body><h1>Welcome to SecD IAM</h1></h1><p>If you did not request this sign up, you can safely ignore this email. Otherwise, please click the following link to validate your account<br/><br/>http://localhost:5500/myapp/iam/exchange/%secd_link%<br/><br/>or use code: %secd_code%</p></body></html>"; + +pub async fn admin_init(is_interactive: bool) -> Result<()> { + let config_dir = util::get_config_dir(); + let config_profile = get_config_profile(); + fs::create_dir_all(config_dir.clone()).await?; + + if config_profile.try_exists()? { + writeln!( + io::stdout(), + "{} {}", + config_profile.clone().display(), + "already exists and there is nothing to initialize. To create a new IAM store use `iam admin create store` or modify the configuration profile directly" + .yellow() + )?; + } else { + writeln!(stdout(), "{}", "creating default profile".green())?; + + let mut login_template = config_dir.clone(); + login_template.push(CONFIG_LOGIN_TEMPLATE); + let mut f = File::create(login_template.clone())?; + f.write_all(DEFAULT_LOGIN_EMAIL.as_bytes())?; + + let mut signup_template = config_dir.clone(); + signup_template.push(CONFIG_SIGNUP_TEMPLATE); + f = File::create(signup_template.clone())?; + f.write_all(DEFAULT_SIGNUP_EMAIL.as_bytes())?; + + let mut cfg = api::Config { + profile: vec![api::ConfigProfile { + name: "default".to_string(), + store: AuthStore::Sqlite, + store_conn: format!( + "sqlite://{}/{}.sql?mode=rwc", + config_dir.clone().display().to_string(), + Alphanumeric.sample_string(&mut rand::thread_rng(), 5), + ), + emailer: secd::AuthEmail::LocalStub, + email_template_login: Some(login_template.display().to_string()), + email_template_signup: Some(signup_template.display().to_string()), + }], + }; + + let mut input: String = String::new(); + if is_interactive { + writeln!(stdout(), "{}", "For a complete overview of configuration options, cancel the initialization and explore `iam help`")?; + write!(stdout(), "Would you like to create a default local store with local stubs for external services?[(y)es/(n)o]: ")?; + stdout().flush()?; + 'outer: loop { + input.clear(); + stdin().read_line(&mut input)?; + match input.as_str().trim() { + "Y" | "y" | "Yes" | "yes" => break, + "N" | "n" | "No" | "no" => { + loop { + write!( + stdout(), + "Persistence store {:?}: ", + AuthStore::VARIANTS + .iter() + .map(|s| s.to_lowercase()) + .collect::<Vec<String>>() + )?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + match AuthStore::from_str(&input.trim()) { + Ok(s) => { + cfg.profile[0].store = s; + break; + } + Err(_) => { + writeln!(stdout(), "{}", "Invalid store type".red())?; + } + } + } + + write!(stdout(), "Store connection string: ")?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + cfg.profile[0].store_conn = input.trim().to_string().clone(); + + loop { + write!( + stdout(), + "Email provider {:?}: ", + AuthEmail::VARIANTS + .iter() + .map(|s| s.to_lowercase()) + .collect::<Vec<String>>() + )?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + match AuthEmail::from_str(&input.trim()) { + Ok(s) => { + cfg.profile[0].emailer = s; + break; + } + Err(_) => { + writeln!(stdout(), "{}", "Invalid email provider".red())?; + } + } + } + + write!( + stdout(), + "Email template for login validation:[FilePath or Enter for default]: " + )?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + cfg.profile[0].email_template_login = + Some(input.trim().to_string().clone()); + + write!( + stdout(), + "Email template for signup validation:[FilePath or Enter for default]: " + )?; + stdout().flush()?; + input.clear(); + stdin().read_line(&mut input)?; + cfg.profile[0].email_template_login = + Some(input.trim().to_string().clone()); + + break 'outer; + } + _ => {} + } + } + } + + let mut f = File::create(config_profile.clone())?; + f.write_all(toml::to_string(&cfg)?.as_bytes())?; + writeln!( + stdout(), + "{} {} {} {} {}", + "created iam config".green(), + "default", + "at".green(), + config_dir.display().to_string(), + "to hold secD iam configurations".green() + )?; + } + Ok(()) +} diff --git a/crates/iam/src/main.rs b/crates/iam/src/main.rs new file mode 100644 index 0000000..c187380 --- /dev/null +++ b/crates/iam/src/main.rs @@ -0,0 +1,287 @@ +mod api; +mod command; +mod util; + +use api::{AdminAction, Args, CliError, Command, CreateObject, GetObject, LinkObject, ListObject}; +use clap::Parser; +use secd::{Secd, SecdError}; +use util::Result; + +use crate::api::ValidationMethod; + +const CONFIG_DIR_NAME: &str = "secdiam"; +const CONFIG_PROFILE_FILE: &str = "profiles.toml"; +const CONFIG_LOGIN_TEMPLATE: &str = "default_login.html"; +const CONFIG_SIGNUP_TEMPLATE: &str = "default_signup.html"; +const ISSUE_TRACKER_LOC: &str = "https://www.github.com/secdiam/iam"; + +#[async_std::main] +async fn main() { + match exec().await { + Ok(Some(s)) => println!("{}", s), + Err(e) => { + println!("{}", e); + std::process::exit(1); + } + _ => {} + } +} + +async fn exec() -> Result<Option<String>> { + let args = Args::parse(); + Ok(match args.command { + Command::Init { interactive } => admin(AdminAction::Init { interactive }) + .await + .map_err(|_| CliError::AdminInitializationError)?, + Command::Admin { action } => admin(action).await?, + + rest @ _ => { + let cfg = util::read_config(args.profile).map_err(|_| CliError::InvalidProfile)?; + let secd = Secd::init( + cfg.store, + Some(&cfg.store_conn), + cfg.emailer, + cfg.email_template_login, + cfg.email_template_signup, + ) + .await + .map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?; + + match rest { + Command::Create { object } => create(&secd, object).await?, + Command::Get { object } => get(&secd, object).await?, + Command::Link { object, unlink } => link(&secd, object, unlink).await?, + Command::Ls { + object, + name, + before, + after, + } => list(&secd, object, name, before, after).await?, + Command::Repl => { + unimplemented!() + } + _ => None, + } + } + }) +} + +async fn admin(cmd: AdminAction) -> Result<Option<String>> { + Ok(match cmd { + AdminAction::Backend { action } => { + println!("do backend stuff!"); + None + } + AdminAction::Create { object } => { + println!("do create!"); + None + } + AdminAction::Init { interactive } => { + command::admin_init(interactive) + .await + .map_err(|_| CliError::AdminInitializationError)?; + None + } + AdminAction::Seal => { + println!("do seal"); + None + } + AdminAction::Unseal { secret_key } => { + println!("do unseal: {}", secret_key); + None + } + }) +} +async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> { + Ok(match cmd { + CreateObject::ApiKey { + identity, + expires_at, + } => { + println!("create object"); + None + } + CreateObject::Group { name, identities } => { + println!("create group"); + None + } + CreateObject::Permission { services, actions } => { + println!("create permission"); + None + } + CreateObject::Role { name, permissions } => { + println!("create role"); + None + } + CreateObject::Service { name, uri } => { + println!("create service"); + None + } + CreateObject::ServiceAction { name, program } => { + println!("create service action"); + None + } + CreateObject::Session { + validation_id, + secret_code, + } => { + let session = secd + .exchange_code_for_session(validation_id, secret_code) + .await + .map_err(|e| match e { + SecdError::InvalidCode => CliError::InvalidCode, + _ => CliError::Unknown, + })?; + serde_json::to_string(&session).ok() + } + CreateObject::Validation { method, identity } => match method { + ValidationMethod::Email { address } => { + secd.create_validation_request(Some(&address)).await?; + None + } + _ => unimplemented!(), + }, + }) +} +async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> { + Ok(match cmd { + GetObject::ApiKey { public_key } => { + println!("get object api key"); + None + } + GetObject::Group { name, id } => { + println!("get object group"); + None + } + GetObject::Identity { id } => { + println!("get object identity"); + None + } + GetObject::Permission { name, id } => { + println!("get object permission"); + None + } + GetObject::Role { name, id } => { + println!("get object role"); + None + } + GetObject::Service { name, id } => { + println!("get object service"); + None + } + GetObject::ServiceAction { name, id } => { + println!("get object service action"); + None + } + GetObject::Session { secret } => { + println!("get object session"); + None + } + GetObject::Validation { id } => { + println!("get object validation"); + None + } + }) +} +async fn link(secd: &Secd, cmd: LinkObject, should_unlink: bool) -> Result<Option<String>> { + Ok(match cmd { + LinkObject::Group { + group_name, + group_id, + identity_ids, + } => { + println!("link object group"); + None + } + LinkObject::Identity { + identity_id, + group_names, + group_ids, + } => { + println!("link object identity"); + None + } + LinkObject::Permission { + permission_name, + permission_id, + role_names, + role_ids, + } => { + println!("link object permission"); + None + } + LinkObject::Role { + role_name, + role_id, + permission_names, + permission_ids, + } => { + println!("link object role"); + None + } + LinkObject::Service { + service_name, + service_id, + permission_names, + permission_ids, + } => { + println!("link object service"); + None + } + LinkObject::ServiceAction { + service_action_name, + service_action_id, + service_name, + service_ids, + } => { + println!("link object service action"); + None + } + }) +} +async fn list( + secd: &Secd, + cmd: ListObject, + filter_name: Option<String>, + filter_before: Option<i64>, + filter_after: Option<i64>, +) -> Result<Option<String>> { + Ok(match cmd { + ListObject::ApiKey => { + println!("list object api key"); + None + } + ListObject::Group => { + println!("list object group"); + None + } + ListObject::Identity => { + println!("list object identity"); + None + } + ListObject::Permission => { + println!("list object permission"); + None + } + ListObject::Role => { + println!("list object role"); + None + } + ListObject::Service => { + println!("list object service"); + None + } + ListObject::ServiceAction => { + println!("list object service action"); + None + } + ListObject::Session => { + println!("list object session"); + None + } + ListObject::Validation => { + println!("list object valiation"); + None + } + }) +} diff --git a/crates/iam/src/util.rs b/crates/iam/src/util.rs new file mode 100644 index 0000000..01ce851 --- /dev/null +++ b/crates/iam/src/util.rs @@ -0,0 +1,88 @@ +use crate::{ + api::{CliError, Config, ConfigProfile}, + CONFIG_DIR_NAME, CONFIG_PROFILE_FILE, ISSUE_TRACKER_LOC, +}; +use anyhow::{anyhow, Context}; +use colored::Colorize; +use home::home_dir; +use secd::Secd; +use std::{ + env::var, + error::Error, + fs::{self, File}, + io::{self, Read}, + path::PathBuf, + result, + str::FromStr, +}; +use thiserror; + +pub type Result<T> = anyhow::Result<T>; + +macro_rules! err { + ($($tt:tt)*) => { Err(Box::<dyn Error>::from(format!($($tt)*))) } +} +pub(crate) use err; + +#[derive(Debug, thiserror::Error)] +pub enum InternalError { + #[error( + "Cannot read {0} profile from {1}. Initialize a default iam store with `iam admin init`" + )] + CannotReadProfile(String, String), +} + +pub fn get_config_dir() -> PathBuf { + let xdg_dir = var("XDG_CONFIG_HOME").map(|s| PathBuf::from_str(&s).unwrap()); + let mut home_dir = home_dir().expect(&format!( + "Could not find home directory. This should not be possible, please file a bug at {}", + ISSUE_TRACKER_LOC + )); + + match xdg_dir { + Ok(mut d) => { + d.push(format!(".{}", CONFIG_DIR_NAME)); + d + } + Err(_) => { + home_dir.push(".config"); + home_dir.push(CONFIG_DIR_NAME); + home_dir + } + } +} + +pub fn get_config_profile() -> PathBuf { + let mut config_dir = get_config_dir(); + config_dir.push(CONFIG_PROFILE_FILE); + config_dir +} + +pub fn read_config(profile_name: Option<String>) -> Result<ConfigProfile> { + let profile_path = get_config_profile(); + let profile_name = profile_name.unwrap_or("default".into()); + + let bytes = fs::read(profile_path.clone())?; + let config: Config = toml::from_slice(&bytes)?; + + let mut cfg = config + .profile + .into_iter() + .filter(|p| p.name == profile_name) + .last() + .ok_or(anyhow!( + "cannot read configuration file when calling read_config" + ))?; + + if let Some(path) = cfg.email_template_login { + let buf = fs::read_to_string(path)?; + cfg.email_template_login = Some(buf); + } + + if let Some(path) = cfg.email_template_signup { + let buf = fs::read_to_string(path)?; + cfg.email_template_signup = Some(buf); + } + + Ok(cfg) +} diff --git a/crates/secd/Cargo.toml b/crates/secd/Cargo.toml new file mode 100644 index 0000000..7e80277 --- /dev/null +++ b/crates/secd/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "secd" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-std = { version = "1.12.0", features = [ "attributes" ] } +async-trait = "0.1" +base64 = "0.13.1" +derive_more = "0.99" +email_address = "0.2" +lazy_static = "1.4" +log = "0.4" +openssl = "0.10.42" +rand = "0.8" +serde = "1" +serde_json = { version = "1.0", features = ["raw_value"] } +strum = "0.24.1" +strum_macros = "0.24" +sqlx = { version = "0.6", features = [ "runtime-async-std-native-tls", "postgres", "uuid", "sqlite", "time" ] } +time = { version = "0.3", features = [ "serde" ] } +thiserror = "1.0" +uuid = { version = "1.2", features = ["v4", "serde"]}
\ No newline at end of file diff --git a/build.rs b/crates/secd/build.rs index 3a8149e..3a8149e 100644 --- a/build.rs +++ b/crates/secd/build.rs diff --git a/crates/secd/src/client/email.rs b/crates/secd/src/client/email.rs new file mode 100644 index 0000000..fc48702 --- /dev/null +++ b/crates/secd/src/client/email.rs @@ -0,0 +1,62 @@ +use std::{path::PathBuf, str::FromStr}; + +use email_address::EmailAddress; + +use super::{ + EmailMessenger, EmailMessengerError, EmailType, EMAIL_TEMPLATE_DEFAULT_LOGIN, + EMAIL_TEMPLATE_DEFAULT_SIGNUP, +}; + +pub(crate) struct LocalEmailStubber { + pub(crate) email_template_login: Option<String>, + pub(crate) email_template_signup: Option<String>, +} + +#[async_trait::async_trait] +impl EmailMessenger for LocalEmailStubber { + // TODO: this module really shouldn't be called client, it should be called services... the client is sqlx/mailgun/sns wrapper or whatever... + async fn send_email( + &self, + email_address: &str, + validation_id: &str, + secret_code: &str, + t: EmailType, + ) -> Result<(), EmailMessengerError> { + let login_template = self + .email_template_login + .clone() + .unwrap_or(EMAIL_TEMPLATE_DEFAULT_LOGIN.to_string()); + let signup_template = self + .email_template_signup + .clone() + .unwrap_or(EMAIL_TEMPLATE_DEFAULT_SIGNUP.to_string()); + + let replace_template = |s: &str| { + s.replace( + "%secd_link%", + &format!("{}?code={}", validation_id, secret_code), + ) + .replace("%secd_email_address%", email_address) + .replace("%secd_code%", secret_code) + }; + + if !EmailAddress::is_valid(email_address) { + return Err(EmailMessengerError::InvalidEmailAddress); + } + + let body = match t { + EmailType::Login => replace_template(&login_template), + EmailType::Signup => replace_template(&signup_template), + }; + + // TODO: write to the system mailbox instead? + std::fs::write( + PathBuf::from_str(&format!("/tmp/{}.localmail", validation_id)) + .map_err(|_| EmailMessengerError::Unknown)?, + body, + ) + .map_err(|_| EmailMessengerError::FailedToSendEmail)?; + + Ok(()) + } +} diff --git a/crates/secd/src/client/mod.rs b/crates/secd/src/client/mod.rs new file mode 100644 index 0000000..3925657 --- /dev/null +++ b/crates/secd/src/client/mod.rs @@ -0,0 +1,209 @@ +pub mod email; +pub mod sqldb; + +use std::collections::HashMap; + +use super::Identity; +use crate::{EmailValidation, Session, SessionSecret}; + +use lazy_static::lazy_static; +use thiserror::Error; +use uuid::Uuid; + +pub enum EmailType { + Login, + Signup, +} + +#[derive(Error, Debug, derive_more::Display)] +pub enum EmailMessengerError { + InvalidEmailAddress, + FailedToSendEmail, + Unknown, +} + +#[async_trait::async_trait] +pub trait EmailMessenger { + async fn send_email( + &self, + email_address: &str, + validation_id: &str, + secret_code: &str, + t: EmailType, + ) -> Result<(), EmailMessengerError>; +} + +#[derive(Error, Debug, derive_more::Display)] +pub enum StoreError { + SqlxError(#[from] sqlx::Error), + EmailAlreadyExists, + CodeAppearsMoreThanOnce, + CodeDoesNotExist(String), + IdentityIdMustExistInvariant, + TooManyEmailValidations, + NoEmailValidationFound, + Unknown, +} + +const EMAIL_TEMPLATE_DEFAULT_LOGIN: &str = "You requested a login link. Please click the following link %secd_code% to login as %secd_email_address%"; +const EMAIL_TEMPLATE_DEFAULT_SIGNUP: &str = "You requested a sign up. Please click the following link %secd_code% to complete your sign up and validate %secd_email_address%"; + +const ERR_MSG_MIGRATION_FAILED: &str = "Failed to execute migrations. This appears to be a secd issue. File a bug at https://www.github.com/secd-lib"; + +const SQLITE: &str = "sqlite"; +const PGSQL: &str = "pgsql"; + +const WRITE_IDENTITY: &str = "write_identity"; +const WRITE_EMAIL_VALIDATION: &str = "write_email_validation"; +const FIND_EMAIL_VALIDATION: &str = "find_email_validation"; + +const WRITE_EMAIL: &str = "write_email"; + +const READ_IDENTITY: &str = "read_identity"; +const FIND_IDENTITY: &str = "find_identity"; +const FIND_IDENTITY_BY_CODE: &str = "find_identity_by_code"; + +const READ_IDENTITY_RAW_ID: &str = "read_identity_raw_id"; +const READ_EMAIL_RAW_ID: &str = "read_email_raw_id"; + +const WRITE_SESSION: &str = "write_session"; +const READ_SESSION: &str = "read_session"; + +lazy_static! { + static ref SQLS: HashMap<&'static str, HashMap<&'static str, &'static str>> = { + let sqlite_sqls: HashMap<&'static str, &'static str> = [ + ( + WRITE_IDENTITY, + include_str!("../../store/sqlite/sql/write_identity.sql"), + ), + ( + WRITE_EMAIL_VALIDATION, + include_str!("../../store/sqlite/sql/write_email_validation.sql"), + ), + ( + WRITE_EMAIL, + include_str!("../../store/sqlite/sql/write_email.sql"), + ), + ( + READ_IDENTITY, + include_str!("../../store/sqlite/sql/read_identity.sql"), + ), + ( + FIND_IDENTITY, + include_str!("../../store/sqlite/sql/find_identity.sql"), + ), + ( + FIND_IDENTITY_BY_CODE, + include_str!("../../store/sqlite/sql/find_identity_by_code.sql"), + ), + ( + READ_IDENTITY_RAW_ID, + include_str!("../../store/sqlite/sql/read_identity_raw_id.sql"), + ), + ( + READ_EMAIL_RAW_ID, + include_str!("../../store/sqlite/sql/read_email_raw_id.sql"), + ), + ( + WRITE_SESSION, + include_str!("../../store/sqlite/sql/write_session.sql"), + ), + ( + READ_SESSION, + include_str!("../../store/sqlite/sql/read_session.sql"), + ), + ( + FIND_EMAIL_VALIDATION, + include_str!("../../store/sqlite/sql/find_email_validation.sql"), + ), + ] + .iter() + .cloned() + .collect(); + + let pg_sqls: HashMap<&'static str, &'static str> = [ + ( + WRITE_IDENTITY, + include_str!("../../store/pg/sql/write_identity.sql"), + ), + ( + WRITE_EMAIL_VALIDATION, + include_str!("../../store/pg/sql/write_email_validation.sql"), + ), + ( + WRITE_EMAIL, + include_str!("../../store/pg/sql/write_email.sql"), + ), + ( + READ_IDENTITY, + include_str!("../../store/pg/sql/read_identity.sql"), + ), + ( + FIND_IDENTITY, + include_str!("../../store/pg/sql/find_identity.sql"), + ), + ( + FIND_IDENTITY_BY_CODE, + include_str!("../../store/pg/sql/find_identity_by_code.sql"), + ), + ( + READ_IDENTITY_RAW_ID, + include_str!("../../store/pg/sql/read_identity_raw_id.sql"), + ), + ( + READ_EMAIL_RAW_ID, + include_str!("../../store/pg/sql/read_email_raw_id.sql"), + ), + ( + WRITE_SESSION, + include_str!("../../store/pg/sql/write_session.sql"), + ), + ( + READ_SESSION, + include_str!("../../store/pg/sql/read_session.sql"), + ), + ( + FIND_EMAIL_VALIDATION, + include_str!("../../store/pg/sql/find_email_validation.sql"), + ), + ] + .iter() + .cloned() + .collect(); + + let sqls: HashMap<&'static str, HashMap<&'static str, &'static str>> = + [(SQLITE, sqlite_sqls), (PGSQL, pg_sqls)] + .iter() + .cloned() + .collect(); + sqls + }; +} + +#[async_trait::async_trait] +pub trait Store { + async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError>; + + async fn find_email_validation( + &self, + validation_id: Option<&Uuid>, + code: Option<&str>, + ) -> Result<EmailValidation, StoreError>; + async fn write_email_validation( + &self, + ev: &EmailValidation, + // TODO: Make this write an EmailValidation + ) -> Result<Uuid, StoreError>; + + async fn find_identity( + &self, + identity_id: Option<&Uuid>, + email: Option<&str>, + ) -> Result<Option<Identity>, StoreError>; + async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError>; + async fn write_identity(&self, i: &Identity) -> Result<(), StoreError>; + async fn read_identity(&self, identity_id: &Uuid) -> Result<Identity, StoreError>; + + async fn write_session(&self, session: &Session) -> Result<(), StoreError>; + async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError>; +} diff --git a/crates/secd/src/client/sqldb.rs b/crates/secd/src/client/sqldb.rs new file mode 100644 index 0000000..6048c48 --- /dev/null +++ b/crates/secd/src/client/sqldb.rs @@ -0,0 +1,424 @@ +use std::sync::Arc; + +use super::{ + EmailValidation, Identity, Session, SessionSecret, Store, StoreError, ERR_MSG_MIGRATION_FAILED, + FIND_EMAIL_VALIDATION, FIND_IDENTITY, FIND_IDENTITY_BY_CODE, PGSQL, READ_EMAIL_RAW_ID, + READ_IDENTITY_RAW_ID, READ_SESSION, SQLITE, SQLS, WRITE_EMAIL, WRITE_EMAIL_VALIDATION, + WRITE_IDENTITY, WRITE_SESSION, +}; +use crate::util; +use log::error; +use openssl::sha::Sha256; +use sqlx::{ + self, database::HasArguments, ColumnIndex, Database, Decode, Encode, Executor, IntoArguments, + Pool, Postgres, Sqlite, Transaction, Type, +}; +use time::OffsetDateTime; +use uuid::Uuid; + +fn get_sqls(root: &str, file: &str) -> Vec<String> { + SQLS.get(root) + .unwrap() + .get(file) + .unwrap() + .split("--") + .map(|p| p.to_string()) + .collect() +} + +fn hash_secret(secret: &str) -> Vec<u8> { + let mut hasher = Sha256::new(); + hasher.update(secret.as_bytes()); + hasher.finish().to_vec() +} + +struct SqlClient<D> +where + D: sqlx::Database, +{ + pool: sqlx::Pool<D>, + sqls_root: String, +} + +impl<D> SqlClient<D> +where + D: sqlx::Database, + for<'c> <D as HasArguments<'c>>::Arguments: IntoArguments<'c, D>, + for<'c> i64: Decode<'c, D> + Type<D>, + for<'c> &'c str: Decode<'c, D> + Type<D>, + for<'c> &'c str: Encode<'c, D> + Type<D>, + for<'c> usize: ColumnIndex<<D as Database>::Row>, + for<'c> Uuid: Decode<'c, D> + Type<D>, + for<'c> Uuid: Encode<'c, D> + Type<D>, + for<'c> &'c Pool<D>: Executor<'c, Database = D>, +{ + async fn read_identity_raw_id(&self, id: &Uuid) -> Result<i64, StoreError> { + let sqls = get_sqls(&self.sqls_root, READ_IDENTITY_RAW_ID); + + Ok(sqlx::query_as::<_, (i64,)>(&sqls[0]) + .bind(id) + .fetch_one(&self.pool) + .await + .map_err(util::log_err_sqlx)? + .0) + } + + async fn read_email_raw_id(&self, address: &str) -> Result<i64, StoreError> { + let sqls = get_sqls(&self.sqls_root, READ_EMAIL_RAW_ID); + + Ok(sqlx::query_as::<_, (i64,)>(&sqls[0]) + .bind(address) + .fetch_one(&self.pool) + .await + .map_err(util::log_err_sqlx)? + .0) + } +} + +#[async_trait::async_trait] +impl<D> Store for SqlClient<D> +where + D: sqlx::Database, + for<'c> <D as HasArguments<'c>>::Arguments: IntoArguments<'c, D>, + for<'c> bool: Decode<'c, D> + Type<D>, + for<'c> bool: Encode<'c, D> + Type<D>, + for<'c> i64: Decode<'c, D> + Type<D>, + for<'c> i64: Encode<'c, D> + Type<D>, + for<'c> i32: Decode<'c, D> + Type<D>, + for<'c> i32: Encode<'c, D> + Type<D>, + for<'c> OffsetDateTime: Decode<'c, D> + Type<D>, + for<'c> OffsetDateTime: Encode<'c, D> + Type<D>, + for<'c> &'c str: ColumnIndex<<D as Database>::Row>, + for<'c> &'c str: Decode<'c, D> + Type<D>, + for<'c> &'c str: Encode<'c, D> + Type<D>, + for<'c> Option<&'c str>: Decode<'c, D> + Type<D>, + for<'c> Option<&'c str>: Encode<'c, D> + Type<D>, + for<'c> String: Decode<'c, D> + Type<D>, + for<'c> String: Encode<'c, D> + Type<D>, + for<'c> Option<String>: Decode<'c, D> + Type<D>, + for<'c> Option<String>: Encode<'c, D> + Type<D>, + for<'c> usize: ColumnIndex<<D as Database>::Row>, + for<'c> Uuid: Decode<'c, D> + Type<D>, + for<'c> Uuid: Encode<'c, D> + Type<D>, + for<'c> &'c [u8]: Encode<'c, D> + Type<D>, + for<'c> Option<&'c Uuid>: Encode<'c, D> + Type<D>, + for<'c> Option<&'c Vec<u8>>: Encode<'c, D> + Type<D>, + for<'c> Option<OffsetDateTime>: Decode<'c, D> + Type<D>, + for<'c> Option<OffsetDateTime>: Encode<'c, D> + Type<D>, + for<'c> &'c Pool<D>: Executor<'c, Database = D>, + for<'c> &'c mut Transaction<'c, D>: Executor<'c, Database = D>, +{ + async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError> { + let sqls = get_sqls(&self.sqls_root, WRITE_EMAIL); + + let identity_id = self.read_identity_raw_id(&identity_id).await?; + + let email_id: (i64,) = match sqlx::query_as(&sqls[0]) + .bind(email_address) + .fetch_one(&self.pool) + .await + { + Ok(i) => i, + Err(sqlx::Error::RowNotFound) => sqlx::query_as::<_, (i64,)>(&sqls[1]) + .bind(email_address) + .fetch_one(&self.pool) + .await + .map_err(util::log_err_sqlx)?, + Err(e) => return Err(StoreError::SqlxError(e)), + }; + + sqlx::query(&sqls[2]) + .bind(identity_id) + .bind(email_id.0) + .bind(OffsetDateTime::now_utc()) + .execute(&self.pool) + .await + .map_err(util::log_err_sqlx)?; + + Ok(()) + } + + async fn find_email_validation( + &self, + validation_id: Option<&Uuid>, + code: Option<&str>, + ) -> Result<EmailValidation, StoreError> { + let sqls = get_sqls(&self.sqls_root, FIND_EMAIL_VALIDATION); + let mut rows = sqlx::query_as::<_, EmailValidation>(&sqls[0]) + .bind(validation_id) + .bind(code) + .fetch_all(&self.pool) + .await + .map_err(util::log_err_sqlx)?; + + match rows.len() { + 0 => Err(StoreError::NoEmailValidationFound), + 1 => Ok(rows.swap_remove(0)), + _ => Err(StoreError::TooManyEmailValidations), + } + } + + async fn write_email_validation(&self, ev: &EmailValidation) -> Result<Uuid, StoreError> { + let sqls = get_sqls(&self.sqls_root, WRITE_EMAIL_VALIDATION); + + let identity_id = self + .read_identity_raw_id( + &ev.identity_id + .ok_or(StoreError::IdentityIdMustExistInvariant)?, + ) + .await?; + let email_id = self.read_email_raw_id(&ev.email_address).await?; + + let new_id = Uuid::new_v4(); + sqlx::query(&sqls[0]) + .bind(ev.id.unwrap_or(new_id)) + .bind(identity_id) + .bind(email_id) + .bind(ev.attempts) + .bind(&ev.code) + .bind(ev.is_validated) + .bind(ev.created_at) + .bind(ev.expires_at) + .execute(&self.pool) + .await + .map_err(util::log_err_sqlx)?; + + Ok(new_id) + } + + async fn find_identity( + &self, + id: Option<&Uuid>, + email: Option<&str>, + ) -> Result<Option<Identity>, StoreError> { + let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY); + Ok( + match sqlx::query_as::<_, Identity>(&sqls[0]) + .bind(id) + .bind(email) + .fetch_one(&self.pool) + .await + { + Ok(i) => Some(i), + Err(sqlx::Error::RowNotFound) => None, + Err(e) => return Err(StoreError::SqlxError(e)), + }, + ) + } + async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> { + let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY_BY_CODE); + + let rows = sqlx::query_as::<_, (i32,)>(&sqls[0]) + .bind(code) + .fetch_all(&self.pool) + .await + .map_err(util::log_err_sqlx)?; + + if rows.len() == 0 { + return Err(StoreError::CodeDoesNotExist(code.to_string())); + } + + if rows.len() != 1 { + return Err(StoreError::CodeAppearsMoreThanOnce); + } + + let identity_email_id = rows.get(0).unwrap().0; + + // TODO: IF we expand beyond email codes, then we'll need to join against a bunch of identity tables. + // but since a single code was found, only one of them should pop... + Ok(sqlx::query_as::<_, Identity>(&sqls[1]) + .bind(identity_email_id) + .fetch_one(&self.pool) + .await + .map_err(util::log_err_sqlx)?) + } + + async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> { + let sqls = get_sqls(&self.sqls_root, WRITE_IDENTITY); + sqlx::query(&sqls[0]) + .bind(i.id) + .bind(i.data.clone()) + .bind(i.created_at) + .execute(&self.pool) + .await + .map_err(|e| { + error!("write_identity_failure"); + error!("{:?}", e); + e + })?; + + Ok(()) + } + async fn read_identity(&self, id: &Uuid) -> Result<Identity, StoreError> { + Ok(sqlx::query_as::<_, Identity>( + " +select identity_public_id, data, created_at from identity where identity_public_id = ?", + ) + .bind(id) + .fetch_one(&self.pool) + .await + .map_err(util::log_err_sqlx)?) + } + + async fn write_session(&self, session: &Session) -> Result<(), StoreError> { + let sqls = get_sqls(&self.sqls_root, WRITE_SESSION); + + let secret_hash = session.secret.as_ref().map(|s| hash_secret(s)); + + sqlx::query(&sqls[0]) + .bind(&session.identity_id) + .bind(secret_hash.as_ref()) + .bind(session.created_at) + .bind(OffsetDateTime::now_utc()) + .bind(session.expires_at) + .bind(session.revoked_at) + .execute(&self.pool) + .await + .map_err(util::log_err_sqlx)?; + + Ok(()) + } + async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError> { + let sqls = get_sqls(&self.sqls_root, READ_SESSION); + + let secret_hash = hash_secret(secret); + let mut session = sqlx::query_as::<_, Session>(&sqls[0]) + .bind(&secret_hash[..]) + .fetch_one(&self.pool) + .await + .map_err(util::log_err_sqlx)?; + + // This should do nothing other than updated touched_at, and then + // clear the plaintext secret + session.secret = Some(secret.to_string()); + self.write_session(&session).await?; + session.secret = None; + + Ok(session) + } +} + +pub struct PgClient { + sql: SqlClient<Postgres>, +} + +impl PgClient { + pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> { + sqlx::migrate!("store/pg/migrations") + .run(&pool) + .await + .expect(ERR_MSG_MIGRATION_FAILED); + + Arc::new(PgClient { + sql: SqlClient { + pool, + sqls_root: PGSQL.to_string(), + }, + }) + } +} + +#[async_trait::async_trait] +impl Store for PgClient { + async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError> { + self.sql.write_email(identity_id, email_address).await + } + async fn find_email_validation( + &self, + validation_id: Option<&Uuid>, + code: Option<&str>, + ) -> Result<EmailValidation, StoreError> { + self.sql.find_email_validation(validation_id, code).await + } + async fn write_email_validation(&self, ev: &EmailValidation) -> Result<Uuid, StoreError> { + self.sql.write_email_validation(ev).await + } + async fn find_identity( + &self, + identity_id: Option<&Uuid>, + email: Option<&str>, + ) -> Result<Option<Identity>, StoreError> { + self.sql.find_identity(identity_id, email).await + } + async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> { + self.sql.find_identity_by_code(code).await + } + async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> { + self.sql.write_identity(i).await + } + async fn read_identity(&self, identity_id: &Uuid) -> Result<Identity, StoreError> { + self.sql.read_identity(identity_id).await + } + async fn write_session(&self, session: &Session) -> Result<(), StoreError> { + self.sql.write_session(session).await + } + async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError> { + self.sql.read_session(secret).await + } +} + +pub struct SqliteClient { + sql: SqlClient<Sqlite>, +} + +impl SqliteClient { + pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> { + sqlx::migrate!("store/sqlite/migrations") + .run(&pool) + .await + .expect(ERR_MSG_MIGRATION_FAILED); + + sqlx::query("pragma foreign_keys = on") + .execute(&pool) + .await + .expect( + "Failed to initialize FK pragma. File a bug at https://www.github.com/secd-lib", + ); + + Arc::new(SqliteClient { + sql: SqlClient { + pool, + sqls_root: SQLITE.to_string(), + }, + }) + } +} + +#[async_trait::async_trait] +impl Store for SqliteClient { + async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError> { + self.sql.write_email(identity_id, email_address).await + } + async fn find_email_validation( + &self, + validation_id: Option<&Uuid>, + code: Option<&str>, + ) -> Result<EmailValidation, StoreError> { + self.sql.find_email_validation(validation_id, code).await + } + async fn write_email_validation(&self, ev: &EmailValidation) -> Result<Uuid, StoreError> { + self.sql.write_email_validation(ev).await + } + async fn find_identity( + &self, + identity_id: Option<&Uuid>, + email: Option<&str>, + ) -> Result<Option<Identity>, StoreError> { + self.sql.find_identity(identity_id, email).await + } + async fn find_identity_by_code(&self, code: &str) -> Result<Identity, StoreError> { + self.sql.find_identity_by_code(code).await + } + async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> { + self.sql.write_identity(i).await + } + async fn read_identity(&self, identity_id: &Uuid) -> Result<Identity, StoreError> { + self.sql.read_identity(identity_id).await + } + async fn write_session(&self, session: &Session) -> Result<(), StoreError> { + self.sql.write_session(session).await + } + async fn read_session(&self, secret: &SessionSecret) -> Result<Session, StoreError> { + self.sql.read_session(secret).await + } +} diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs new file mode 100644 index 0000000..9eb7f0e --- /dev/null +++ b/crates/secd/src/lib.rs @@ -0,0 +1,409 @@ +mod client; +mod util; + +use std::sync::Arc; + +use client::{ + email, + sqldb::{PgClient, SqliteClient}, + EmailMessenger, EmailMessengerError, Store, StoreError, +}; +use derive_more::Display; +use email_address::EmailAddress; +use log::error; +use rand::distributions::{Alphanumeric, DistString}; +use serde::{Deserialize, Serialize}; +use strum_macros::{EnumString, EnumVariantNames}; +use time::{Duration, OffsetDateTime}; +use uuid::Uuid; + +const SESSION_SIZE_BYTES: usize = 32; +const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */; +const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */; +const VALIDATION_ATTEMPTS_MAX: i32 = 5; +const VALIDATION_CODE_SIZE: usize = 6; + +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct ApiKey { + pub public_key: String, + pub private_key: String, +} + +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct Authorization { + session: Session, +} + +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct Identity { + #[sqlx(rename = "identity_public_id")] + id: Uuid, + created_at: OffsetDateTime, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option<String>, +} + +#[derive(sqlx::FromRow, Debug, Serialize)] +pub struct Session { + #[sqlx(rename = "identity_public_id")] + identity_id: IdentityId, + #[serde(skip_serializing_if = "Option::is_none")] + #[sqlx(default)] + secret: Option<SessionSecret>, + #[serde(with = "time::serde::timestamp")] + created_at: OffsetDateTime, + #[serde(with = "time::serde::timestamp")] + expires_at: OffsetDateTime, + #[serde(skip_serializing_if = "Option::is_none")] + revoked_at: Option<OffsetDateTime>, +} + +#[derive(sqlx::FromRow, Debug)] +pub struct EmailValidation { + #[sqlx(rename = "email_validation_public_id")] + id: Option<Uuid>, + #[sqlx(rename = "identity_public_id")] + identity_id: Option<IdentityId>, + #[sqlx(rename = "address")] + email_address: String, + attempts: i32, + code: String, + is_validated: bool, + created_at: OffsetDateTime, + expires_at: OffsetDateTime, + revoked_at: Option<OffsetDateTime>, +} + +#[derive(Copy, Display, Clone, Debug)] +pub enum OauthProvider { + Amazon, + Apple, + Dropbox, + Facebook, + Github, + Gitlab, + Google, + Instagram, + LinkedIn, + Microsoft, + Paypal, + Reddit, + Spotify, + Strava, + Stripe, + Twitch, + Twitter, + WeChat, +} + +#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] +#[strum(ascii_case_insensitive)] +pub enum AuthStore { + Sqlite, + Postgres, + MySql, + Mongo, + Dynamo, + Redis, +} + +#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)] +#[strum(ascii_case_insensitive)] +pub enum AuthEmail { + LocalStub, + Ses, + Mailgun, + Sendgrid, +} + +pub type IdentityId = Uuid; +pub type SessionSecret = String; +pub type SessionSecretHash = String; +pub type ValidationRequestId = Uuid; +pub type ValidationSecretCode = String; + +#[derive(Debug, derive_more::Display, thiserror::Error)] +pub enum SecdError { + InvalidEmailAddress, + InvalidCode, + InitializationFailure(sqlx::Error), + IdentityIdShouldExistInvariant, + EmailSendError(#[from] EmailMessengerError), + EmailValidationRequestError, + EmailValidationExpiryOverflow, + SessionExpiryOverflow, + Unauthenticated, + Unknown, +} + +pub struct Secd { + store: Arc<dyn Store + Send + Sync + 'static>, + email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>, +} + +impl Secd { + pub async fn init( + auth_store: AuthStore, + conn_string: Option<&str>, + email_messenger: AuthEmail, + email_template_login: Option<String>, + email_template_signup: Option<String>, + ) -> Result<Self, SecdError> { + let store = match auth_store { + AuthStore::Sqlite => { + SqliteClient::new( + sqlx::sqlite::SqlitePoolOptions::new() + .connect(conn_string.unwrap_or("sqlite::memory:".into())) + .await + .map_err(|e| SecdError::InitializationFailure(e))?, + ) + .await + } + AuthStore::Postgres => { + PgClient::new( + sqlx::postgres::PgPoolOptions::new() + .connect(conn_string.expect("No postgres connection string provided.")) + .await + .map_err(|e| SecdError::InitializationFailure(e))?, + ) + .await + } + rest @ _ => { + error!( + "requested an AuthStore which has not yet been implemented: {:?}", + rest + ); + unimplemented!() + } + }; + + let email_sender = match email_messenger { + // TODO: initialize email and SMS templates with secd + AuthEmail::LocalStub => email::LocalEmailStubber { + email_template_login, + email_template_signup, + }, + _ => unimplemented!(), + }; + + Ok(Secd { + store, + email_messenger: Arc::new(email_sender), + }) + } + /// create_validation_request + /// + /// Generate a request to validate the provided email. + pub async fn create_validation_request( + &self, + email: Option<&str>, + ) -> Result<ValidationRequestId, SecdError> { + let now = OffsetDateTime::now_utc(); + + let email = match email { + Some(ea) => { + if EmailAddress::is_valid(ea) { + ea + } else { + return Err(SecdError::InvalidEmailAddress); + } + } + None => return Err(SecdError::InvalidEmailAddress), + }; + + let mut ev = EmailValidation { + id: None, + identity_id: None, + email_address: email.to_string(), + attempts: 0, + code: Alphanumeric + .sample_string(&mut rand::thread_rng(), VALIDATION_CODE_SIZE) + .to_lowercase(), + is_validated: false, + created_at: now, + expires_at: now + .checked_add(Duration::new(EMAIL_VALIDATION_DURATION, 0)) + .ok_or(SecdError::EmailValidationExpiryOverflow)?, + revoked_at: None, + }; + + let (req_id, mail_type) = match self + .store + .find_identity(None, Some(email)) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))? + { + Some(identity) => { + let req_id = { + ev.identity_id = Some(identity.id); + self.store + .write_email_validation(&ev) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))? + }; + (req_id, client::EmailType::Login) + } + None => { + let identity = Identity { + id: Uuid::new_v4(), + created_at: OffsetDateTime::now_utc(), + data: None, + }; + self.store + .write_identity(&identity) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; + self.store + .write_email(identity.id, email) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; + + let req_id = { + ev.identity_id = Some(identity.id); + self.store + .write_email_validation(&ev) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))? + }; + + (req_id, client::EmailType::Signup) + } + }; + + self.email_messenger + .send_email(email, &req_id.to_string(), &ev.code, mail_type) + .await?; + + Ok(req_id) + } + /// exchange_secret_for_session + /// + /// Exchanges a secret, which consists of a validation_request_id and secret_code + /// for a session which allows authentication on behalf of the associated identity. + /// + /// Session secrets should be used to return authorization for the associated identity. + pub async fn exchange_code_for_session( + &self, + validation_request_id: ValidationRequestId, + code: ValidationSecretCode, + ) -> Result<Session, SecdError> { + let mut ev = self + .store + .find_email_validation(Some(&validation_request_id), Some(&code)) + .await + .map_err(|e| util::log_err(e.into(), SecdError::EmailValidationExpiryOverflow))?; + + if ev.is_validated + || ev.expires_at < OffsetDateTime::now_utc() + || ev.attempts >= VALIDATION_ATTEMPTS_MAX + { + return Err(SecdError::InvalidCode); + }; + + ev.is_validated = true; + ev.attempts += 1; + self.store + .write_email_validation(&ev) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; + + // TODO: clear previous sessions if they fit the criteria + let now = OffsetDateTime::now_utc(); + let s = Session { + identity_id: ev + .identity_id + .ok_or(SecdError::IdentityIdShouldExistInvariant)?, + secret: Some(Alphanumeric.sample_string(&mut rand::thread_rng(), SESSION_SIZE_BYTES)), + created_at: now, + expires_at: now + .checked_add(Duration::new(SESSION_DURATION, 0)) + .ok_or(SecdError::SessionExpiryOverflow)?, + revoked_at: None, + }; + self.store + .write_session(&s) + .await + .map_err(|e| util::log_err(e.into(), SecdError::Unknown))?; + + Ok(s) + } + /// get_identity + /// + /// Return all information associated with the identity id. + pub async fn get_identity(&self, identity: IdentityId) -> Result<Identity, SecdError> { + unimplemented!() + } + /// get_authorization + /// + /// Return the authorization for this session. If the session is + /// invalid, expired or otherwise unauthenticated, an error will + /// be returned. + pub async fn get_authorization( + &self, + secret: SessionSecret, + ) -> Result<Authorization, SecdError> { + match self.store.read_session(&secret).await { + Ok(session) + if session.expires_at > OffsetDateTime::now_utc() + || session.revoked_at > Some(OffsetDateTime::now_utc()) => + { + Ok(Authorization { session }) + } + Ok(_) => Err(SecdError::Unauthenticated), + Err(_e) => Err(SecdError::Unknown), + } + } + /// revoke_session + /// + /// Revokes a session such that it may no longer be used to authenticate + /// the associated identity. + pub async fn revoke_session(&self, secret_hash: SessionSecretHash) -> Result<(), SecdError> { + unimplemented!() + } + /// revoke_identity + /// + /// Soft delete an identity such that all associated resources are + /// deleted as well. + /// + /// NOTE: This operation cannot be undone. Although it may not be undone + /// a separate call to delete_identity is required to cleanup necessary + /// resources. + /// + /// You may configure secd to periodically clean all revoked + /// identities and associated resources with AUTOCLEAN_REVOKED. + pub async fn revoke_identity(&self, identity_id: IdentityId) -> Result<(), SecdError> { + unimplemented!() + } + /// delete_identity + /// + /// Delete an identity and all associated resources (e.g. session, + /// authorization structures, etc...). This is a hard delete and permanently + /// removes all stored information. + /// + /// NOTE: An identity _must_ be revoked before it can be deleted. Otherwise, + /// secd will return an error. + pub async fn delete_identity(&self, identity_id: IdentityId) -> Result<(), SecdError> { + unimplemented!() + } + + // register service + // register service_action(service_id, action) + // list services + // list service actions + + // create permission + // create group (name, identities) + // create role (name, permissios) + // list group + // list role + // list permission + // describe group + // describe role + // describe permission + // add_identity_to_group + // remove_identity_from_group + // add_permission_to_role + // remove_permission_from_role + // attach_role_to_group + // attach_permission_to_group (just creates single role and attaches it) +} diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs new file mode 100644 index 0000000..da16901 --- /dev/null +++ b/crates/secd/src/util/mod.rs @@ -0,0 +1,21 @@ +use log::error; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + +use crate::SecdError; + +pub(crate) fn log_err(e: Box<dyn std::error::Error>, new_e: SecdError) -> SecdError { + error!("{:?}", e); + new_e +} +pub(crate) fn log_err_sqlx(e: sqlx::Error) -> sqlx::Error { + error!("{:?}", e); + e +} +pub(crate) fn generate_random_url_safe(n: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(n) + .map(char::from) + .collect() +} diff --git a/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql b/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql new file mode 100644 index 0000000..7a1bf07 --- /dev/null +++ b/crates/secd/store/pg/migrations/20221116062550_bootstrap.sql @@ -0,0 +1,49 @@ +create extension if not exists pgcrypto; +create extension if not exists citext; +create schema if not exists auth; + +create table if not exists auth.identity ( + identity_id bigserial primary key + , identity_public_id uuid + , data text + , created_at timestamptz not null + , unique(identity_public_id) +); + +create table if not exists auth.email ( + email_id bigserial primary key + , address text not null + , unique(address) +); + +create table if not exists auth.identity_email ( + identity_email_id bigserial primary key + , identity_id bigint not null references auth.identity(identity_id) + , email_id bigint not null references auth.email(email_id) + , created_at timestamptz not null + , deleted_at timestamptz +); + +create table if not exists auth.email_validation ( + email_validation_id bigserial primary key + , email_validation_public_id uuid not null + , identity_email_id integer not null references auth.identity_email(identity_email_id) + , attempts integer not null + , code text + , is_validated boolean not null default false + , created_at timestamptz not null + , expires_at timestamptz + , revoked_at timestamptz + , unique(email_validation_public_id) +); + +create table if not exists auth.session ( + session_id bigserial primary key + , identity_id bigint not null references auth.identity(identity_id) + , secret_hash bytea not null + , created_at timestamptz not null + , touched_at timestamptz not null + , expires_at timestamptz + , revoked_at timestamptz + , unique(secret_hash) +); diff --git a/crates/secd/store/pg/sql/find_email_validation.sql b/crates/secd/store/pg/sql/find_email_validation.sql new file mode 100644 index 0000000..d16d8e7 --- /dev/null +++ b/crates/secd/store/pg/sql/find_email_validation.sql @@ -0,0 +1,17 @@ +select + ev.email_validation_public_id + , i.identity_public_id + , e.address + , ev.attempts + , ev.code + , ev.is_validated + , ev.created_at + , ev.expires_at + , ev.revoked_at +from auth.email_validation ev +join auth.identity_email ie using (identity_email_id) +join auth.email e using (email_id) +join auth.identity i using (identity_id) +where (($1 is null) or (email_validation_public_id = $1)) +and (($2 is null) or (code = $2)); +-- diff --git a/crates/secd/store/pg/sql/find_identity.sql b/crates/secd/store/pg/sql/find_identity.sql new file mode 100644 index 0000000..3a86a83 --- /dev/null +++ b/crates/secd/store/pg/sql/find_identity.sql @@ -0,0 +1,9 @@ +select + identity_public_id, + data, + i.created_at +from auth.identity i +join auth.identity_email ie using (identity_id) +join auth.email e using (email_id) +where (($1 is null) or (i.identity_public_id = $1)) +and (($2 is null) or (e.address = $2)) diff --git a/crates/secd/store/pg/sql/find_identity_by_code.sql b/crates/secd/store/pg/sql/find_identity_by_code.sql new file mode 100644 index 0000000..9df6614 --- /dev/null +++ b/crates/secd/store/pg/sql/find_identity_by_code.sql @@ -0,0 +1,11 @@ +select identity_email_id +from auth.email_validation +where email_validation_public_id = $1::uuid +-- +select + identity_public_id + , data + , i.created_at +from auth.identity i +left join auth.identity_email ie using (identity_id) +where ie.identity_email_id = $1; diff --git a/crates/secd/store/pg/sql/read_email_raw_id.sql b/crates/secd/store/pg/sql/read_email_raw_id.sql new file mode 100644 index 0000000..f62331c --- /dev/null +++ b/crates/secd/store/pg/sql/read_email_raw_id.sql @@ -0,0 +1 @@ +select email_id from auth.email where address = $1 diff --git a/crates/secd/store/pg/sql/read_identity.sql b/crates/secd/store/pg/sql/read_identity.sql new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/crates/secd/store/pg/sql/read_identity.sql diff --git a/crates/secd/store/pg/sql/read_identity_raw_id.sql b/crates/secd/store/pg/sql/read_identity_raw_id.sql new file mode 100644 index 0000000..d550cc0 --- /dev/null +++ b/crates/secd/store/pg/sql/read_identity_raw_id.sql @@ -0,0 +1,2 @@ +select identity_id from auth.identity where identity_public_id = $1; +-- diff --git a/crates/secd/store/pg/sql/read_session.sql b/crates/secd/store/pg/sql/read_session.sql new file mode 100644 index 0000000..febc1ab --- /dev/null +++ b/crates/secd/store/pg/sql/read_session.sql @@ -0,0 +1,8 @@ +select + i.identity_public_id + , s.created_at + , s.expires_at + , s.revoked_at +from auth.session s +join auth.identity i using (identity_id) +where secret_hash = $1; diff --git a/crates/secd/store/pg/sql/write_email.sql b/crates/secd/store/pg/sql/write_email.sql new file mode 100644 index 0000000..75fc494 --- /dev/null +++ b/crates/secd/store/pg/sql/write_email.sql @@ -0,0 +1,11 @@ +insert into auth.email ( + address +) values ( + $1 +) on conflict (address) do nothing +returning email_id; +-- +select email_id from auth.email where address = $1; +-- +insert into auth.identity_email (identity_id, email_id, created_at) values ($1, $2, $3); +-- diff --git a/crates/secd/store/pg/sql/write_email_validation.sql b/crates/secd/store/pg/sql/write_email_validation.sql new file mode 100644 index 0000000..98fc60e --- /dev/null +++ b/crates/secd/store/pg/sql/write_email_validation.sql @@ -0,0 +1,27 @@ +insert into auth.email_validation + ( + email_validation_public_id + , identity_email_id + , attempts + , code + , is_validated + , created_at + , expires_at + ) +values ( + $1 + , ( + select identity_email_id + from auth.identity_email + where identity_id = $2 + and email_id = $3 + ) + , $4 + , $5 + , $6 + , $7 + , $8 +) on conflict (email_validation_public_id) do update + set attempts = excluded.attempts + , is_validated = excluded.is_validated + , expires_at = excluded.expires_at; diff --git a/crates/secd/store/pg/sql/write_identity.sql b/crates/secd/store/pg/sql/write_identity.sql new file mode 100644 index 0000000..eed1710 --- /dev/null +++ b/crates/secd/store/pg/sql/write_identity.sql @@ -0,0 +1,9 @@ +insert into auth.identity ( + identity_public_id, + data, + created_at +) values ( + $1, + $2, + $3 +); diff --git a/crates/secd/store/pg/sql/write_session.sql b/crates/secd/store/pg/sql/write_session.sql new file mode 100644 index 0000000..cd5892b --- /dev/null +++ b/crates/secd/store/pg/sql/write_session.sql @@ -0,0 +1,18 @@ +insert into auth.session ( + identity_id + , secret_hash + , created_at + , touched_at + , expires_at + , revoked_at +) values ( + (select identity_id from auth.identity where identity_public_id = $1) + , $2 + , $3 + , $4 + , $5 + , $6 +) on conflict (secret_hash) do update + set touched_at = excluded.touched_at + , revoked_at = excluded.revoked_at; +-- diff --git a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql new file mode 100644 index 0000000..aa95afc --- /dev/null +++ b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql @@ -0,0 +1,45 @@ +create table if not exists identity ( + identity_id integer primary key autoincrement + , identity_public_id uuid + , data text + , created_at timestamp not null + , unique(identity_public_id) +); + +create table if not exists email ( + email_id integer primary key autoincrement + , address text not null + , unique(address) +); + +create table if not exists identity_email ( + identity_email_id integer primary key autoincrement + , identity_id integer not null references identity(identity_id) + , email_id integer not null references email(email_id) + , created_at timestamp not null + , deleted_at timestamp +); + +create table if not exists email_validation ( + email_validation_id integer primary key autoincrement + , email_validation_public_id text not null -- uuid + , identity_email_id integer not null references identity_email(identity_email_id) + , attempts integer not null + , code text + , is_validated boolean not null + , created_at timestamp not null + , expires_at timestamp + , revoked_at timestamp + , unique(email_validation_public_id) +); + +create table if not exists session ( + session_id integer primary key autoincrement + , identity_id not null references identity(identity_id) + , secret_hash blob not null + , created_at timestamp not null + , touched_at timestamp not null + , expires_at timestamp + , revoked_at timestamp + , unique(secret_hash) +); diff --git a/crates/secd/store/sqlite/sql/find_email_validation.sql b/crates/secd/store/sqlite/sql/find_email_validation.sql new file mode 100644 index 0000000..a34c149 --- /dev/null +++ b/crates/secd/store/sqlite/sql/find_email_validation.sql @@ -0,0 +1,16 @@ +select + ev.email_validation_public_id + , i.identity_public_id + , e.address + , ev.attempts + , ev.code + , ev.is_validated + , ev.created_at + , ev.expires_at + , ev.revoked_at +from email_validation ev +join identity_email ie using (identity_email_id) +join email e using (email_id) +join identity i using (identity_id) +where ((?1 is null) or (email_validation_public_id = ?1)) +and ((?2 is null) or (code = ?2)); diff --git a/crates/secd/store/sqlite/sql/find_identity.sql b/crates/secd/store/sqlite/sql/find_identity.sql new file mode 100644 index 0000000..bd1654d --- /dev/null +++ b/crates/secd/store/sqlite/sql/find_identity.sql @@ -0,0 +1,9 @@ +select + identity_public_id, + data, + i.created_at +from identity i +join identity_email ie using (identity_id) +join email e using (email_id) +where ((?1 is null) or (i.identity_public_id = ?1)) +and ((?2 is null) or (e.address = ?2)) diff --git a/crates/secd/store/sqlite/sql/find_identity_by_code.sql b/crates/secd/store/sqlite/sql/find_identity_by_code.sql new file mode 100644 index 0000000..e1a6050 --- /dev/null +++ b/crates/secd/store/sqlite/sql/find_identity_by_code.sql @@ -0,0 +1,11 @@ +select identity_email_id +from auth.email_validation +where email_validation_public_id = ?1; +-- +select + identity_public_id + , data + , i.created_at +from auth.identity i +left join auth.identity_email ie using (identity_id) +where ie.identity_email_id = ?1; diff --git a/crates/secd/store/sqlite/sql/read_email_raw_id.sql b/crates/secd/store/sqlite/sql/read_email_raw_id.sql new file mode 100644 index 0000000..0bbafad --- /dev/null +++ b/crates/secd/store/sqlite/sql/read_email_raw_id.sql @@ -0,0 +1 @@ +select email_id from email where address = ? diff --git a/crates/secd/store/sqlite/sql/read_identity.sql b/crates/secd/store/sqlite/sql/read_identity.sql new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/crates/secd/store/sqlite/sql/read_identity.sql diff --git a/crates/secd/store/sqlite/sql/read_identity_raw_id.sql b/crates/secd/store/sqlite/sql/read_identity_raw_id.sql new file mode 100644 index 0000000..552c570 --- /dev/null +++ b/crates/secd/store/sqlite/sql/read_identity_raw_id.sql @@ -0,0 +1,2 @@ +select identity_id from identity where identity_public_id = ?; +-- diff --git a/crates/secd/store/sqlite/sql/read_session.sql b/crates/secd/store/sqlite/sql/read_session.sql new file mode 100644 index 0000000..4daa352 --- /dev/null +++ b/crates/secd/store/sqlite/sql/read_session.sql @@ -0,0 +1,8 @@ +select + i.identity_public_id + , s.created_at + , s.expires_at + , s.revoked_at +from session s +join identity i using (identity_id) +where secret_hash = ?1; diff --git a/crates/secd/store/sqlite/sql/write_email.sql b/crates/secd/store/sqlite/sql/write_email.sql new file mode 100644 index 0000000..c127d9c --- /dev/null +++ b/crates/secd/store/sqlite/sql/write_email.sql @@ -0,0 +1,11 @@ +insert into email ( + address +) values ( + ?1 +) on conflict (address) do nothing +returning email_id; +-- +select email_id from email where email = ?1; +-- +insert into identity_email (identity_id, email_id, created_at) values (?1, ?2, ?3); +-- diff --git a/crates/secd/store/sqlite/sql/write_email_validation.sql b/crates/secd/store/sqlite/sql/write_email_validation.sql new file mode 100644 index 0000000..37b13e1 --- /dev/null +++ b/crates/secd/store/sqlite/sql/write_email_validation.sql @@ -0,0 +1,27 @@ +insert into email_validation + ( + email_validation_public_id + , identity_email_id + , attempts + , code + , is_validated + , created_at + , expires_at + ) +values ( + ?1 + , ( + select identity_email_id + from identity_email + where identity_id = ?2 + and email_id = ?3 + ) + , ?4 + , ?5 + , ?6 + , ?7 + , ?8 +) on conflict (email_validation_public_id) do update + set attempts = excluded.attempts + , is_validated = excluded.is_validated + , expires_at = excluded.expires_at; diff --git a/crates/secd/store/sqlite/sql/write_identity.sql b/crates/secd/store/sqlite/sql/write_identity.sql new file mode 100644 index 0000000..ff54468 --- /dev/null +++ b/crates/secd/store/sqlite/sql/write_identity.sql @@ -0,0 +1 @@ +insert into identity (identity_public_id, data, created_at) values (?1, ?2, ?3); diff --git a/crates/secd/store/sqlite/sql/write_session.sql b/crates/secd/store/sqlite/sql/write_session.sql new file mode 100644 index 0000000..3c26986 --- /dev/null +++ b/crates/secd/store/sqlite/sql/write_session.sql @@ -0,0 +1,18 @@ +insert into session ( + identity_id + , secret_hash + , created_at + , touched_at + , expires_at + , revoked_at +) values ( + (select identity_id from identity where identity_public_id = ?1) + , ?2 + , ?3 + , ?4 + , ?5 + , ?6 +) on conflict (secret_hash) do update + set touched_at = excluded.touched_at + , revoked_at = excluded.revoked_at; +-- diff --git a/justfile b/justfile new file mode 100644 index 0000000..e41eef4 --- /dev/null +++ b/justfile @@ -0,0 +1,5 @@ +run: + RUST_BACKTRACE=1 cargo run + +build: + cargo build --release diff --git a/landing/index.html b/landing/index.html index 87099b7..cec5846 100644 --- a/landing/index.html +++ b/landing/index.html @@ -5,7 +5,13 @@ </head> <body> <h1> - Sec(urity)D(aemon) + SecD </h1> + <h2> + Identity and Access Management enabled by the Sec(urity)D(aemon) + </h2> + <div> + This is pretty cool. You should use it. + </div> </body> </html> diff --git a/src/client/mod.rs b/src/client/mod.rs deleted file mode 100644 index bb32e2c..0000000 --- a/src/client/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -pub mod sqldb; - -use thiserror::Error; -use uuid::Uuid; - -use super::Identity; - -#[derive(Error, Debug)] -pub enum StoreError { - #[error("sqlx client error")] - SqlxError(#[from] sqlx::Error), - #[error( - "More than one oauth provider identified, but no client_id was provided for disambiguation" - )] - TooManyOauthProviders, - #[error("Oath provider not registered. First register the Oauth provider before executing")] - OauthProviderNotRegistered, - #[error("An unknown error occurred")] - Unknown, -} - -#[async_trait::async_trait] -pub trait Store { - // async fn read_oauth_authorization_location( - // &self, - // provider: OauthProvider, - // client_id: Option<OauthClientId>, - // ) -> Result<String, StoreError>; - - // async fn write_oauth_authorization_request( - // &self, - // identity_id: Uuid, - // provider: OauthProvider, - // raw: String, - // state: String, - // ) -> Result<(), StoreError>; - - // async fn write_oauth_provider( - // &self, - // provider: OauthProvider, - // consent_uri: OauthConsentUri, - // client_id: OauthClientId, - // client_secret: OauthClientSecretEncrypted, - // redirect_uri: String, - // ) -> Result<(), StoreError>; - - // fn read_email_challenge(&self) -> Result<T, StoreError>; - // fn write_email_challenge(&self) -> Result<T, StoreError>; - - async fn write_email(&self, id: Uuid, email_address: &str) -> Result<(), StoreError>; - async fn write_email_validation_request( - &self, - id: Uuid, - email_address: &str, - ) -> Result<Uuid, StoreError>; - - async fn find_identity( - &self, - id: Option<&Uuid>, - email: Option<&str>, - ) -> Result<Option<Identity>, StoreError>; - async fn write_identity(&self, i: &Identity) -> Result<(), StoreError>; - async fn read_identity(&self, id: &Uuid) -> Result<Identity, StoreError>; - - // fn read_sms_challenge(&self) -> Result<T, StoreError>; - // fn write_sms_challenge(&self) -> Result<T, StoreError>; -} - -// #[derive(sqlx::FromRow, Debug)] -// struct Identity { -// #[sqlx(rename = "identity_public_id")] -// id: Uuid, -// } - -// #[derive(sqlx::FromRow, Debug)] -// struct OauthProviderRecord { -// consent_uri: String, -// client_id: OauthClientId, -// client_secret_encrypted: OauthClientSecretEncrypted, -// redirect_uri: String, -// } diff --git a/src/client/sqldb.rs b/src/client/sqldb.rs deleted file mode 100644 index 6ad0cc1..0000000 --- a/src/client/sqldb.rs +++ /dev/null @@ -1,172 +0,0 @@ -use log::{debug, error}; -use uuid::Uuid; - -use crate::{util, Identity}; - -use super::{Store, StoreError}; - -pub struct SqliteClient { - pool: sqlx::Pool<sqlx::Sqlite>, -} - -impl SqliteClient { - pub async fn new(pool: sqlx::Pool<sqlx::Sqlite>) -> Self { - sqlx::migrate!("store/sqlite/migrations") - .run(&pool) - .await - .expect( - "Failed to execute migrations. This appears to be a secd issue. File a bug at https://www.github.com/secd-lib" - ); - - sqlx::query("pragma foreign_keys = on") - .execute(&pool) - .await - .expect( - "Failed to initialize FK pragma. File a bug at https://www.github.com/secd-lib", - ); - - SqliteClient { pool } - } -} - -impl SqliteClient { - async fn read_identity_raw_id(&self, id: &Uuid) -> Result<i64, StoreError> { - Ok(sqlx::query_as::<_, (i64,)>( - " -select identity_id from identity where identity_public_id = ?", - ) - .bind(id) - .fetch_one(&self.pool) - .await - .map_err(util::log_err)? - .0) - } - - async fn read_email_raw_id(&self, address: &str) -> Result<i64, StoreError> { - Ok(sqlx::query_as::<_, (i64,)>( - " -select email_id from email where address = ?", - ) - .bind(address) - .fetch_one(&self.pool) - .await - .map_err(util::log_err)? - .0) - } -} - -#[async_trait::async_trait] -impl Store for SqliteClient { - async fn write_email(&self, identity_id: Uuid, email_address: &str) -> Result<(), StoreError> { - let mut tx = self.pool.begin().await?; - - let identity_id = self.read_identity_raw_id(&identity_id).await?; - - let email_id: (i64,) = sqlx::query_as( - " -insert into email (address) values (?) returning email_id", - ) - .bind(email_address) - .fetch_one(&mut tx) - .await - .map_err(util::log_err)?; - - debug!("identity: {}, email: {}", identity_id, email_id.0); - - sqlx::query( - " -insert into identity_email (identity_id, email_id) values (?,?);", - ) - .bind(identity_id) - .bind(email_id.0) - .execute(&mut tx) - .await - .map_err(util::log_err)?; - - tx.commit().await?; - - Ok(()) - } - - async fn write_email_validation_request( - &self, - identity_id: Uuid, - email_address: &str, - ) -> Result<Uuid, StoreError> { - let identity_id = self.read_identity_raw_id(&identity_id).await?; - let email_id = self.read_email_raw_id(email_address).await?; - - let request_id = Uuid::new_v4(); - sqlx::query(" -insert into email_validation_request (email_validation_request_public_id, identity_email_id, is_validated) -values (?, (select identity_email_id from identity_email where identity_id = ? and email_id =?), ?)", - ) - .bind(request_id) - .bind(identity_id) - .bind(email_id) - .bind(false) - .execute(&self.pool) - .await - .map_err(util::log_err)?; - - Ok(request_id) - } - - async fn write_identity(&self, i: &Identity) -> Result<(), StoreError> { - sqlx::query( - " -insert into identity (identity_public_id, data, created_at) values (?, ?, ?)", - ) - .bind(i.id) - .bind(i.data.clone()) - .bind(i.created_at) - .execute(&self.pool) - .await - .map_err(|e| { - error!("{:?}", e); - e - })?; - - Ok(()) - } - - async fn read_identity(&self, id: &Uuid) -> Result<Identity, StoreError> { - Ok(sqlx::query_as::<_, Identity>( - " -select identity_public_id, data, created_at from identity where identity_public_id = ?", - ) - .bind(id) - .fetch_one(&self.pool) - .await - .map_err(util::log_err)?) - } - - async fn find_identity( - &self, - id: Option<&Uuid>, - email: Option<&str>, - ) -> Result<Option<Identity>, StoreError> { - Ok( - match sqlx::query_as::<_, Identity>( - " -select identity_public_id, data, i.created_at -from identity i -join identity_email ie using (identity_id) -join email e using (email_id) -where ((? is null) or (i.identity_public_id = ?)) -and ((? is null) or (e.address = ?));", - ) - .bind(id) - .bind(id) - .bind(email) - .bind(email) - .fetch_one(&self.pool) - .await - { - Ok(i) => Some(i), - Err(sqlx::Error::RowNotFound) => None, - Err(e) => return Err(StoreError::SqlxError(e)), - }, - ) - } -} diff --git a/src/ipl/authn.rs b/src/ipl/authn.rs deleted file mode 100644 index 8b13789..0000000 --- a/src/ipl/authn.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/ipl/authz.rs b/src/ipl/authz.rs deleted file mode 100644 index f2f23e4..0000000 --- a/src/ipl/authz.rs +++ /dev/null @@ -1 +0,0 @@ -// TODO: Authorization suite diff --git a/src/ipl/mod.rs b/src/ipl/mod.rs deleted file mode 100644 index 1946995..0000000 --- a/src/ipl/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod authn; -pub mod authz; diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 7856d5c..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,171 +0,0 @@ -mod client; -mod util; - -use std::sync::Arc; - -use client::{sqldb::SqliteClient, Store, StoreError}; -use derive_more::Display; -use email_address::EmailAddress; -use log::{error, info}; -use serde::Serialize; -use time::OffsetDateTime; -use uuid::Uuid; - -#[derive(Copy, Display, Clone, Debug)] -pub enum OauthProvider { - Amazon, - Apple, - Dropbox, - Facebook, - Github, - Gitlab, - Google, - Instagram, - LinkedIn, - Microsoft, - Paypal, - Reddit, - Spotify, - Strava, - Stripe, - Twitch, - Twitter, - WeChat, -} - -#[derive(Display, Debug)] -pub enum AuthStore { - Sqlite, - Postgres, - MySql, - Mongo, - Dynamo, - Redis, -} - -pub type OauthClientId = String; -pub type OauthClientSecretEncrypted = String; -pub type OauthConsentUri = String; - -pub type IdentityId = Uuid; - -////////////////////////////////////////////////// -// Resources -#[derive(sqlx::FromRow, Debug, Serialize)] -pub struct Identity { - #[sqlx(rename = "identity_public_id")] - id: Uuid, - created_at: sqlx::types::time::OffsetDateTime, - #[serde(skip_serializing_if = "Option::is_none")] - data: Option<String>, -} - -pub struct ApiKey {} - -pub struct Session {} - -pub struct ValidationRequest {} -////////////////////////////////////////////////// -#[derive(Debug, derive_more::Display, thiserror::Error)] -pub enum SecdError { - InvalidEmailAddress, - SqliteInitializationFailure(sqlx::Error), - StoreError(#[from] StoreError), - EmailValidationRequestError, - Unknown, -} - -pub struct Secd { - store: Arc<dyn Store + Send + Sync + 'static>, -} - -impl Secd { - pub async fn init( - auth_store: AuthStore, - conn_string: Option<String>, - // TODO: Turn Secd into a trait and impl separately. - // TODO: initialize email and SMS templates with secd - ) -> Result<Self, SecdError> { - let store = match auth_store { - AuthStore::Sqlite => SqliteClient::new( - sqlx::sqlite::SqlitePoolOptions::new() - .connect(conn_string.unwrap_or("sqlite::memory:".into()).as_str()) - .await - .map_err(|e| SecdError::SqliteInitializationFailure(e))?, - ), - // TODO: if AuthStore is provided, then configure the client. - _ => return Err(SecdError::Unknown), - } - .await; - - Ok(Secd { - store: Arc::new(store), - }) - } - - pub async fn create_identity(&self) -> Result<Uuid, SecdError> { - let id = Uuid::new_v4(); - self.store - .write_identity(&Identity { - id, - created_at: OffsetDateTime::now_utc(), - data: None, - }) - .await?; - - Ok(id) - } - - pub async fn create_validation_request(&self, email: Option<&str>) -> Result<(), SecdError> { - // TODO: refactor based on email, phone, or some other template? Or break up the API? - let email = match email { - Some(ea) => { - if EmailAddress::is_valid(ea) { - ea - } else { - return Err(SecdError::InvalidEmailAddress); - } - } - None => return Err(SecdError::InvalidEmailAddress), - }; - - match self.store.find_identity(None, Some(email)).await? { - Some(identity) => { - error!("TODO: implement email send with LOGIN template"); - error!("TODO: send to: {}", email); - let req_id = self - .store - .write_email_validation_request(identity.id, email) - .await?; - - // TODO: provide some dummy email handlers that are used when testing locally... - error!("TODO: when the request comes back, it needs to hit something like /iam/identity/1234/email-validation/1234?code=2345"); - error!("TODO: consequently, we may want to shorten the url by providing a quick access code and/or /iam/email-validation/1234/validate"); - } - None => { - let identity = Identity { - id: Uuid::new_v4(), - created_at: OffsetDateTime::now_utc(), - data: None, - }; - self.store.write_identity(&identity).await?; - self.store.write_email(identity.id, email).await?; - error!("TODO: implement email send with SIGN_UP template"); - self.store - .write_email_validation_request(identity.id, email) - .await?; - } - } - - error!("TODO: think about returning the identity id for which this validation request was created"); - Ok(()) - } - - pub async fn get_identity(&self, id: IdentityId) -> Result<Identity, SecdError> { - Ok(self.store.read_identity(&id).await?) - } - - pub async fn create_email_validation(email: String) -> Result<(), SecdError> { - Ok(()) - } -} diff --git a/src/main.old b/src/main.old deleted file mode 100644 index e9d36c5..0000000 --- a/src/main.old +++ /dev/null @@ -1,112 +0,0 @@ -mod api; -mod client; -mod service; -mod util; - -use std::error::Error; - -use client::sqldb::PostgresClient; -use env_logger::Env; -use service::authn::Authn; -use sqlx::postgres::PgPoolOptions; - -#[async_std::main] -async fn main() -> Result<(), Box<dyn Error>> { - env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init(); - - // Load configuration - // which DB do you want to use? - // what is the connection string (e.g. location, pass, etc...)? - - let pool = PgPoolOptions::new() - .max_connections(5) - .connect("postgres://secduser:p4ssw0rd@localhost:5419/secd") - .await?; - - sqlx::migrate!("store/sql/migrations").run(&pool).await?; - - // there are a few routes - // the service itself just provides some local functions which may be wrapped in a server. - // if you want to use the server, then you start the java/python/rust/ruby/go/etc... server - // otherwise, you just bring in the java/python/rust/ruby/go/etc... client - // also...maybe a terraform template to launch a _minimal_ auth server - // with your choice of RDS, dynamo, bigquery, or even local sqlite... - - // obviously need to configure terraform things... - - // if using the server, then you need to configure a few things: - // oauth endpoint with response_type, client_id - - // scratch - let pg_client = Box::new(PostgresClient::new(pool)); - let authn = Authn { store: pg_client }; - - ////////////////////////////////////////////////// - // CREATE NEW IDENTITY // which would be saved by the client - let identity = authn.register_identity().await?; - - ////////////////////////////////////////////////// - // Register a new oauth provider with some secrets, redirect, ids, etc... - authn - .register_oauth_provider( - api::OauthProvider::Google, - format!("client_id_{}", "CLIENT_SECRET_123"), - format!("client_secret_{}", util::generate_random_url_safe(4)), - "https://iam.SOMESITE.com/goauth...provided by default or customized".to_string(), - ) - .await?; - - ////////////////////////////////////////////////// - // Start oauth challenge and return the appropriate location. - let loc = authn - .initiate_oauth_challenge(identity, api::OauthProvider::Google) - .await?; - - ////////////////////////////////////////////////// - // Complete oauth challenge and return a session token - // let session = authn - // .complete_oauth_challenge(identity, api::OauthProvider::Google, state, access_token, expires_at, raw); - - ////////////////////////////////////////////////// - // Start email challenge - // authn.initiate_email_challenge(identity, email_address); - - ////////////////////////////////////////////////// - // Complete email challenge - // let session = authn.complete_email_challenge(email_address, code); - - ////////////////////////////////////////////////// - // Start SMS challenge - // authn.initiate_sms_challenge(identity, phone_number); - - ////////////////////////////////////////////////// - // Complete SMS challenge - // let session = authn.complete_sms_challenge(phone_number, code); - - ////////////////////////////////////////////////// - // Validate credentials - // let session = authn.validate(username, passphrase); - - ////////////////////////////////////////////////// - // Revoke session - // authn.revoke_session(token); - - ////////////////////////////////////////////////// - // Create API key - // let pub, priv = authn.generate_api_key(identity, Some(expires_at)); - - ////////////////////////////////////////////////// - // Revoke API key - // authn.revoke_api_key(pub, priv); - - ////////////////////////////////////////////////// - // Revoke identity - // authn.revoke_identity(identity); - - println!("Oauth2.0 URL: {}", loc); - - Ok(()) -} - -// TODO: oauth flow -// TODO: email flow diff --git a/src/util/mod.rs b/src/util/mod.rs deleted file mode 100644 index c939b95..0000000 --- a/src/util/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -use log::error; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; - -pub fn log_err(e: sqlx::Error) -> sqlx::Error { - error!("{:?}", e); - e -} -pub fn generate_random_url_safe(n: usize) -> String { - thread_rng() - .sample_iter(&Alphanumeric) - .take(n) - .map(char::from) - .collect() -} diff --git a/store/psql/migrations/20221116062550_bootstrap.sql b/store/psql/migrations/20221116062550_bootstrap.sql deleted file mode 100644 index fd64958..0000000 --- a/store/psql/migrations/20221116062550_bootstrap.sql +++ /dev/null @@ -1,97 +0,0 @@ -create extension if not exists pgcrypto; -create extension if not exists citext; -create schema if not exists auth; - -create table if not exists auth.identity ( - identity_id bigserial primary key - , identity_public_id uuid default gen_random_uuid() - , created_at timestamp not null default current_timestamp - , unique(identity_public_id) -); - -create table if not exists auth.email ( - email_id bigserial primary key - , address text not null -); - -create table if not exists auth.email_challenge_request ( - email_challenge_request_id bigserial primary key - , email_id bigint not null references auth.email(email_id) - , code text not null - , created_at timestamp not null default current_timestamp - , expires_at timestamp - , revoked_at timestamp -); - -create table if not exists auth.email_challenge_response ( - email_challenge_response_id bigserial primary key - , email_challenge_request_id bigint not null references auth.email_challenge_request(email_challenge_request_id) - , is_valid bool not null - , raw_response text not null - , created_at timestamp -); - -create table if not exists auth.identity_email ( - identity_id bigint not null references auth.identity(identity_id) - , email_id bigint not null references auth.email(email_id) - , created_at timestamp not null default current_timestamp - , deleted_at timestamp -); - -create table if not exists auth.phone_number ( - phone_number_id bigserial primary key - , digits text not null -); - -create table if not exists auth.phone_number_challenge_request ( - phone_number_challenge_request_id bigserial primary key - , phone_number_id bigint not null references auth.phone_number(phone_number_id) - , code text not null - , created_at timestamp not null default current_timestamp - , expires_at timestamp - , revoked_at timestamp -); - -create table if not exists auth.phone_number_challenge_response ( - phone_number_challenge_response_id bigserial primary key - , phone_number_challenge_request_id bigint not null references auth.phone_number_challenge_request(phone_number_challenge_request_id) - , is_valid bool not null - , raw_response text not null - , created_at timestamp -); - -create table if not exists auth.identity_phone_number ( - identity_id bigint not null references auth.identity(identity_id) - , phone_number_id bigint not null references auth.phone_number(phone_number_id) - , created_at timestamp not null default current_timestamp - , deleted_at timestamp -); - -create table if not exists auth.oauth_provider ( - oauth_provider_id bigserial primary key - , provider text not null - , consent_uri text not null - , client_id text not null - , client_secret_encrypted text not null - , redirect_uri text - , created_at timestamp not null default current_timestamp - , unique(provider, client_id) -); - -create table if not exists auth.oauth_request ( - oauth_request_id bigserial primary key - , oauth_provider_id bigint not null references auth.oauth_provider(oauth_provider_id) - , identity_id bigint not null references auth.identity(identity_id) - , state text not null - , created_at timestamp not null default current_timestamp -); - -create table if not exists auth.oauth_response ( - oauth_response_id bigserial primary key - , oauth_request_id bigint not null references auth.oauth_request(oauth_request_id) - , is_error boolean not null - , raw_response text not null - , access_token text not null - , expires_at timestamp not null - , created_at timestamp not null default current_timestamp -); diff --git a/store/sqlite/migrations/20221125051738_bootstrap.sql b/store/sqlite/migrations/20221125051738_bootstrap.sql deleted file mode 100644 index 70a8892..0000000 --- a/store/sqlite/migrations/20221125051738_bootstrap.sql +++ /dev/null @@ -1,32 +0,0 @@ -create table if not exists identity ( - identity_id integer primary key autoincrement - , identity_public_id uuid - , data text default '{}' - , created_at timestamp not null default current_timestamp - , unique(identity_public_id) -); - -create table if not exists email ( - email_id integer primary key autoincrement - , address text not null - , unique(address) -); - -create table if not exists identity_email ( - identity_email_id integer primary key autoincrement - , identity_id integer not null references identity(identity_id) - , email_id integer not null references email(email_id) - , created_at timestamp not null default current_timestamp - , deleted_at timestamp not null default current_timestamp -); - -create table if not exists email_validation_request ( - email_validation_request_id integer primary key autoincrement - -- uuid - , email_validation_request_public_id text not null - , identity_email_id integer not null references identity_email(identity_email_id) - , is_validated boolean not null default false - , created_at timestamp not null default current_timestamp - , expires_at timestamp - , revoked_at timestamp -); |
