Improve cookie/token handling (#1153)

This commit is contained in:
Firstyear 2022-10-31 10:50:04 +10:00 committed by GitHub
parent fb1a67681a
commit db75a0b344
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 870 additions and 547 deletions

246
Cargo.lock generated
View file

@ -68,7 +68,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.7",
"getrandom 0.2.8",
"once_cell",
"version_check",
]
@ -99,9 +99,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anyhow"
version = "1.0.65"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
[[package]]
name = "anymap2"
@ -134,7 +134,7 @@ dependencies = [
"num-traits",
"rusticata-macros",
"thiserror",
"time 0.3.15",
"time 0.3.16",
]
[[package]]
@ -173,9 +173,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.3.14"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695"
checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
dependencies = [
"flate2",
"futures-core",
@ -210,9 +210,9 @@ dependencies = [
[[package]]
name = "async-global-executor"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da5b41ee986eed3f524c380e6d64965aea573882a8907682ad100f7859305ca"
checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
dependencies = [
"async-channel",
"async-executor",
@ -242,16 +242,16 @@ dependencies = [
[[package]]
name = "async-io"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7"
checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7"
dependencies = [
"async-lock",
"autocfg",
"concurrent-queue",
"futures-lite",
"libc",
"log",
"once_cell",
"parking",
"polling",
"slab",
@ -262,11 +262,12 @@ dependencies = [
[[package]]
name = "async-lock"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6"
checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685"
dependencies = [
"event-listener",
"futures-lite",
]
[[package]]
@ -583,9 +584,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.11.0"
version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "byte-tools"
@ -625,9 +626,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.0.73"
version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
dependencies = [
"jobserver",
]
@ -750,6 +751,16 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@ -858,7 +869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917"
dependencies = [
"percent-encoding",
"time 0.3.15",
"time 0.3.16",
"version_check",
]
@ -874,7 +885,7 @@ dependencies = [
"publicsuffix",
"serde",
"serde_json",
"time 0.3.15",
"time 0.3.16",
"url",
]
@ -1075,9 +1086,9 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.1.23"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn",
@ -1092,6 +1103,50 @@ dependencies = [
"cipher",
]
[[package]]
name = "cxx"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a"
[[package]]
name = "cxxbridge-macro"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "daemon"
version = "1.1.0-alpha.9"
@ -1113,9 +1168,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.14.1"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
dependencies = [
"darling_core",
"darling_macro",
@ -1123,9 +1178,9 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.14.1"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
dependencies = [
"fnv",
"ident_case",
@ -1137,9 +1192,9 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.14.1"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
dependencies = [
"darling_core",
"quote",
@ -1386,7 +1441,7 @@ checksum = "c6dedfc944f4ac38cac8b74cb1c7b4fb73c175db232d6fa98e9bd1fd81908b89"
dependencies = [
"base64 0.13.1",
"byteorder",
"getrandom 0.2.7",
"getrandom 0.2.8",
"openssl",
"zeroize",
]
@ -1596,9 +1651,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
@ -1818,9 +1873,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.14"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
dependencies = [
"bytes",
"fnv",
@ -1919,7 +1974,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
dependencies = [
"bytes",
"fnv",
"itoa 1.0.3",
"itoa 1.0.4",
]
[[package]]
@ -1994,7 +2049,7 @@ dependencies = [
"http-body",
"httparse",
"httpdate",
"itoa 1.0.3",
"itoa 1.0.4",
"pin-project-lite 0.2.9",
"socket2",
"tokio",
@ -2031,17 +2086,28 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.50"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -2157,9 +2223,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
[[package]]
name = "jobserver"
@ -2477,9 +2543,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.135"
version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libgit2-sys"
@ -2508,9 +2574,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.25.1"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f0455f2c1bc9a7caa792907026e469c1d91761fb0ea37cbb16427c77280cf35"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
dependencies = [
"cc",
"pkg-config",
@ -2563,6 +2629,15 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "link-cplusplus"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
dependencies = [
"cc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@ -2679,14 +2754,14 @@ dependencies = [
[[package]]
name = "mio"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.36.1",
"windows-sys 0.42.0",
]
[[package]]
@ -2855,7 +2930,7 @@ checksum = "6d62c436394991641b970a92e23e8eeb4eb9bca74af4f5badc53bcd568daadbd"
dependencies = [
"base64 0.13.1",
"chrono",
"getrandom 0.2.7",
"getrandom 0.2.8",
"http",
"rand 0.8.5",
"reqwest",
@ -2878,9 +2953,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.15.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "oncemutex"
@ -2940,9 +3015,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.76"
version = "0.9.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce"
checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a"
dependencies = [
"autocfg",
"cc",
@ -2981,9 +3056,9 @@ dependencies = [
[[package]]
name = "os_str_bytes"
version = "6.3.0"
version = "6.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9"
[[package]]
name = "overload"
@ -3018,15 +3093,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.36.1",
"windows-sys 0.42.0",
]
[[package]]
@ -3154,9 +3229,9 @@ dependencies = [
[[package]]
name = "polling"
version = "2.3.0"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011"
checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
@ -3226,9 +3301,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.46"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
]
@ -3250,11 +3325,11 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "publicsuffix"
version = "2.2.2"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeeedb0b429dc462f30ad27ef3de97058b060016f47790c066757be38ef792b4"
checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457"
dependencies = [
"idna 0.2.3",
"idna 0.3.0",
"psl-types",
]
@ -3372,7 +3447,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.7",
"getrandom 0.2.8",
]
[[package]]
@ -3423,7 +3498,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.7",
"getrandom 0.2.8",
"redox_syscall",
"thiserror",
]
@ -3688,6 +3763,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scratch"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "sct"
version = "0.7.0"
@ -3823,7 +3904,7 @@ version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
dependencies = [
"itoa 1.0.3",
"itoa 1.0.4",
"ryu",
"serde",
]
@ -3855,7 +3936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa 1.0.3",
"itoa 1.0.4",
"ryu",
"serde",
]
@ -4328,16 +4409,24 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.15"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c"
checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca"
dependencies = [
"itoa 1.0.3",
"itoa 1.0.4",
"libc",
"num_threads",
"time-macros 0.2.4",
"serde",
"time-core",
"time-macros 0.2.5",
]
[[package]]
name = "time-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "time-macros"
version = "0.1.1"
@ -4350,9 +4439,12 @@ dependencies = [
[[package]]
name = "time-macros"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b"
dependencies = [
"time-core",
]
[[package]]
name = "time-macros-impl"
@ -4588,9 +4680,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-normalization"
@ -4663,7 +4755,7 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
dependencies = [
"getrandom 0.2.7",
"getrandom 0.2.8",
"serde",
]
@ -5156,7 +5248,7 @@ dependencies = [
"oid-registry",
"rusticata-macros",
"thiserror",
"time 0.3.15",
"time 0.3.16",
]
[[package]]
@ -5276,5 +5368,5 @@ dependencies = [
"lazy_static",
"quick-error",
"regex",
"time 0.3.15",
"time 0.3.16",
]

View file

@ -393,7 +393,7 @@ pub struct UserAuthToken {
pub displayname: String,
pub spn: String,
pub mail_primary: Option<String>,
pub groups: Vec<Group>,
// pub groups: Vec<Group>,
pub ui_hints: BTreeSet<UiHint>,
}
@ -414,9 +414,11 @@ impl fmt::Display for UserAuthToken {
writeln!(f, "purpose: read write (expiry: {})", expiry)?
}
}
/*
for group in &self.groups {
writeln!(f, "group: {:?}", group.spn)?;
}
*/
Ok(())
}
}
@ -866,11 +868,24 @@ impl fmt::Display for AuthMech {
}
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum AuthIssueSession {
Token,
Cookie,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuthStep {
// name
Init(String),
// A new way to issue sessions. Doing this as a new init type
// to prevent breaking existing clients.
Init2 {
username: String,
issue: AuthIssueSession,
},
// We want to talk to you like this.
Begin(AuthMech),
// Step
@ -959,9 +974,11 @@ pub enum AuthState {
Continue(Vec<AuthAllowed>),
// Something was bad, your session is terminated and no cookie.
Denied(String),
// Everything is good, your bearer header has been issued and is within
// Everything is good, your bearer token has been issued and is within
// the result.
Success(String),
// Everything is good, your cookie has been issued.
SuccessCookie,
}
#[derive(Debug, Serialize, Deserialize)]

View file

@ -6,7 +6,7 @@ use std::sync::Arc;
use kanidm_proto::v1::{
ApiToken, AuthRequest, BackupCodesView, CURequest, CUSessionToken, CUStatus, CredentialStatus,
Entry as ProtoEntry, OperationError, RadiusAuthToken, SearchRequest, SearchResponse, UatStatus,
UnixGroupToken, UnixUserToken, WhoamiResponse,
UnixGroupToken, UnixUserToken, UserAuthToken, WhoamiResponse,
};
use ldap3_proto::simple::*;
use regex::Regex;
@ -320,6 +320,34 @@ impl QueryServerReadV1 {
}
}
#[instrument(
level = "info",
name = "whoami_uat",
skip(self, uat, eventid)
fields(uuid = ?eventid)
)]
pub async fn handle_whoami_uat(
&self,
uat: Option<String>,
eventid: Uuid,
) -> Result<UserAuthToken, OperationError> {
let ct = duration_from_epoch_now();
let idms_prox_read = self.idms.proxy_read().await;
// Make an event from the whoami request. This will process the event and
// generate a selfuuid search.
//
// This current handles the unauthenticated check, and will
// trigger the failure, but if we can manage to work out async
// then move this to core.rs, and don't allow Option<UAT> to get
// this far.
idms_prox_read
.validate_and_parse_token_to_uat(uat.as_deref(), ct)
.map_err(|e| {
admin_error!(?e, "Invalid identity");
e
})
}
#[instrument(
level = "info",
skip_all,

View file

@ -378,11 +378,11 @@ pub fn create_https_server(
tserver.with(sketching::middleware::TreeMiddleware::new(
trust_x_forward_for,
));
// tserver.with(tide::log::LogMiddleware::new());
// We do not force a session ttl, because we validate this elsewhere in usage.
tserver.with(
// We do not force a session ttl, because we validate this elsewhere in usage.
tide::sessions::SessionMiddleware::new(tide::sessions::MemoryStore::new(), cookie_key)
tide::sessions::SessionMiddleware::new(tide::sessions::CookieStore::new(), cookie_key)
.with_cookie_name("kanidm-session")
.with_same_site_policy(tide::http::cookies::SameSite::Strict),
);
@ -566,6 +566,7 @@ pub fn create_https_server(
let mut self_route = appserver.at("/v1/self");
self_route.at("/").mapped_get(&mut routemap, whoami);
self_route.at("/_uat").mapped_get(&mut routemap, whoami_uat);
self_route
.at("/_attr/:attr")

View file

@ -3,9 +3,10 @@ use std::time::Duration;
use compact_jwt::Jws;
use kanidm_proto::v1::{
AccountUnixExtend, ApiTokenGenerate, AuthRequest, AuthResponse, AuthState as ProtoAuthState,
CUIntentToken, CURequest, CUSessionToken, CreateRequest, DeleteRequest, Entry as ProtoEntry,
GroupUnixExtend, ModifyRequest, OperationError, SearchRequest, SingleStringRequest,
AccountUnixExtend, ApiTokenGenerate, AuthIssueSession, AuthRequest, AuthResponse,
AuthState as ProtoAuthState, CUIntentToken, CURequest, CUSessionToken, CreateRequest,
DeleteRequest, Entry as ProtoEntry, GroupUnixExtend, ModifyRequest, OperationError,
SearchRequest, SingleStringRequest,
};
use kanidmd_lib::filter::{Filter, FilterInvalid};
use kanidmd_lib::idm::event::AuthResult;
@ -64,10 +65,25 @@ pub async fn whoami(req: tide::Request<AppState>) -> tide::Result {
to_tide_response(res, hvalue)
}
pub async fn logout(req: tide::Request<AppState>) -> tide::Result {
pub async fn whoami_uat(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let (eventid, hvalue) = req.new_eventid();
let res = req.state().qe_r_ref.handle_whoami_uat(uat, eventid).await;
to_tide_response(res, hvalue)
}
pub async fn logout(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let (eventid, hvalue) = req.new_eventid();
// Now lets nuke any cookies for the session. We do this before the handle_logout
// so that if any errors occur, the cookies are still removed.
let msession = req.session_mut();
msession.remove("auth-session-id");
msession.remove("bearer");
let res = req.state().qe_w_ref.handle_logout(uat, eventid).await;
to_tide_response(res, hvalue)
}
@ -1125,17 +1141,21 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
})
.map(|_| ProtoAuthState::Continue(allowed))
}
AuthState::Success(token) => {
AuthState::Success(token, issue) => {
debug!("🧩 -> AuthState::Success");
// Remove the auth-session-id
let msession = req.session_mut();
msession.remove("auth-session-id");
// Create a session cookie?
msession.remove("bearer");
msession
.insert("bearer", token.clone())
match issue {
AuthIssueSession::Cookie => msession
.insert("bearer", token)
.map_err(|_| OperationError::InvalidSessionState)
.map(|_| ProtoAuthState::Success(token))
.map(|_| ProtoAuthState::SuccessCookie),
AuthIssueSession::Token => Ok(ProtoAuthState::Success(token)),
}
}
AuthState::Denied(reason) => {
debug!("🧩 -> AuthState::Denied");

View file

@ -213,7 +213,7 @@ impl Account {
mail_primary: self.mail_primary.clone(),
ui_hints: self.ui_hints.clone(),
// application: None,
groups: self.groups.iter().map(|g| g.to_proto()).collect(),
// groups: self.groups.iter().map(|g| g.to_proto()).collect(),
})
}

View file

@ -10,7 +10,9 @@ use std::time::Duration;
// use webauthn_rs::proto::Credential as WebauthnCredential;
use compact_jwt::{Jws, JwsSigner};
use hashbrown::HashSet;
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthMech, AuthType, OperationError};
use kanidm_proto::v1::{
AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, AuthType, OperationError,
};
// use crossbeam::channel::Sender;
use tokio::sync::mpsc::UnboundedSender as Sender;
use uuid::Uuid;
@ -569,13 +571,21 @@ pub(crate) struct AuthSession {
//
// This handler will then handle the mfa and stepping up through to generate the auth states
state: AuthSessionState,
// The type of session we will issue if successful
issue: AuthIssueSession,
}
impl AuthSession {
/// Create a new auth session, based on the available credential handlers of the account.
/// the session is a whole encapsulated unit of what we need to proceed, so that subsequent
/// or interleved write operations do not cause inconsistency in this process.
pub fn new(account: Account, webauthn: &Webauthn, ct: Duration) -> (Option<Self>, AuthState) {
pub fn new(
account: Account,
issue: AuthIssueSession,
webauthn: &Webauthn,
ct: Duration,
) -> (Option<Self>, AuthState) {
// During this setup, determine the credential handler that we'll be using
// for this session. This is currently based on presentation of an application
// id.
@ -623,7 +633,11 @@ impl AuthSession {
(None, AuthState::Denied(reason.to_string()))
} else {
// We can proceed
let auth_session = AuthSession { account, state };
let auth_session = AuthSession {
account,
state,
issue,
};
// Get the set of mechanisms that can proceed. This is tied
// to the session so that it can mutate state and have progression
// of what's next, or ordering.
@ -742,8 +756,11 @@ impl AuthSession {
CredState::Success(auth_type) => {
security_info!("Successful cred handling");
let session_id = Uuid::new_v4();
let issue = self.issue;
security_info!(
"Starting session {} for {} {}",
"Issuing {:?} session {} for {} {}",
issue,
session_id,
self.account.spn,
self.account.uuid
@ -805,7 +822,7 @@ impl AuthSession {
(
Some(AuthSessionState::Success),
Ok(AuthState::Success(token)),
Ok(AuthState::Success(token, issue)),
)
}
CredState::Continue(allowed) => {
@ -870,7 +887,7 @@ mod tests {
use compact_jwt::JwsSigner;
use hashbrown::HashSet;
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthMech};
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
use tokio::sync::mpsc::unbounded_channel as unbounded;
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::WebauthnAuthenticator;
@ -914,7 +931,12 @@ mod tests {
let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1);
let (session, state) = AuthSession::new(anon_account, &webauthn, duration_from_epoch_now());
let (session, state) = AuthSession::new(
anon_account,
AuthIssueSession::Token,
&webauthn,
duration_from_epoch_now(),
);
if let AuthState::Choose(auth_mechs) = state {
assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Anonymous)));
@ -942,8 +964,12 @@ mod tests {
$account:expr,
$webauthn:expr
) => {{
let (session, state) =
AuthSession::new($account.clone(), $webauthn, duration_from_epoch_now());
let (session, state) = AuthSession::new(
$account.clone(),
AuthIssueSession::Token,
$webauthn,
duration_from_epoch_now(),
);
let mut session = session.unwrap();
if let AuthState::Choose(auth_mechs) = state {
@ -1013,7 +1039,7 @@ mod tests {
Some(&pw_badlist_cache),
&jws_signer,
) {
Ok(AuthState::Success(_)) => {}
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
@ -1066,8 +1092,12 @@ mod tests {
$account:expr,
$webauthn:expr
) => {{
let (session, state) =
AuthSession::new($account.clone(), $webauthn, duration_from_epoch_now());
let (session, state) = AuthSession::new(
$account.clone(),
AuthIssueSession::Token,
$webauthn,
duration_from_epoch_now(),
);
let mut session = session.expect("Session was unable to be created.");
if let AuthState::Choose(auth_mechs) = state {
@ -1260,7 +1290,7 @@ mod tests {
Some(&pw_badlist_cache),
&jws_signer,
) {
Ok(AuthState::Success(_)) => {}
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
@ -1347,8 +1377,12 @@ mod tests {
$account:expr,
$webauthn:expr
) => {{
let (session, state) =
AuthSession::new($account.clone(), $webauthn, duration_from_epoch_now());
let (session, state) = AuthSession::new(
$account.clone(),
AuthIssueSession::Token,
$webauthn,
duration_from_epoch_now(),
);
let mut session = session.unwrap();
if let AuthState::Choose(auth_mechs) = state {
@ -1485,7 +1519,7 @@ mod tests {
None,
&jws_signer,
) {
Ok(AuthState::Success(_)) => {}
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
@ -1724,7 +1758,7 @@ mod tests {
Some(&pw_badlist_cache),
&jws_signer,
) {
Ok(AuthState::Success(_)) => {}
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
@ -1931,7 +1965,7 @@ mod tests {
Some(&pw_badlist_cache),
&jws_signer,
) {
Ok(AuthState::Success(_)) => {}
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
@ -1970,7 +2004,7 @@ mod tests {
Some(&pw_badlist_cache),
&jws_signer,
) {
Ok(AuthState::Success(_)) => {}
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
@ -2127,7 +2161,7 @@ mod tests {
Some(&pw_badlist_cache),
&jws_signer,
) {
Ok(AuthState::Success(_)) => {}
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
}
@ -2169,7 +2203,7 @@ mod tests {
Some(&pw_badlist_cache),
&jws_signer,
) {
Ok(AuthState::Success(_)) => {}
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
_ => panic!(),
};
}

View file

@ -1473,7 +1473,7 @@ mod tests {
use std::time::Duration;
use async_std::task;
use kanidm_proto::v1::{AuthAllowed, AuthMech, CredentialDetailType};
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, CredentialDetailType};
use uuid::uuid;
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::WebauthnAuthenticator;
@ -1714,7 +1714,7 @@ mod tests {
match r2 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token),
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// Process the auth session
@ -1788,7 +1788,7 @@ mod tests {
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token),
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// Process the auth session
@ -1857,7 +1857,7 @@ mod tests {
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token),
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// There now should be a backup code invalidation present
@ -1934,7 +1934,7 @@ mod tests {
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token),
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// Process the webauthn update

View file

@ -3,7 +3,7 @@ use std::time::Duration;
use crate::idm::AuthState;
use crate::prelude::*;
use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::{AuthCredential, AuthMech, AuthRequest, AuthStep};
use kanidm_proto::v1::{AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthStep};
#[cfg(test)]
use webauthn_rs::prelude::PublicKeyCredential;
@ -294,8 +294,8 @@ impl LdapTokenAuthEvent {
#[derive(Debug)]
pub struct AuthEventStepInit {
pub name: String,
pub appid: Option<String>,
pub username: String,
pub issue: AuthIssueSession,
}
#[derive(Debug)]
@ -320,9 +320,14 @@ pub enum AuthEventStep {
impl AuthEventStep {
fn from_authstep(aus: AuthStep, sid: Option<Uuid>) -> Result<Self, OperationError> {
match aus {
AuthStep::Init(name) => {
Ok(AuthEventStep::Init(AuthEventStepInit { name, appid: None }))
AuthStep::Init(username) => Ok(AuthEventStep::Init(AuthEventStepInit {
username,
issue: AuthIssueSession::Token,
})),
AuthStep::Init2 { username, issue } => {
Ok(AuthEventStep::Init(AuthEventStepInit { username, issue }))
}
AuthStep::Begin(mech) => match sid {
Some(ssid) => Ok(AuthEventStep::Begin(AuthEventStepMech {
sessionid: ssid,
@ -347,16 +352,16 @@ impl AuthEventStep {
#[cfg(test)]
pub fn anonymous_init() -> Self {
AuthEventStep::Init(AuthEventStepInit {
name: "anonymous".to_string(),
appid: None,
username: "anonymous".to_string(),
issue: AuthIssueSession::Token,
})
}
#[cfg(test)]
pub fn named_init(name: &str) -> Self {
AuthEventStep::Init(AuthEventStepInit {
name: name.to_string(),
appid: None,
username: name.to_string(),
issue: AuthIssueSession::Token,
})
}

View file

@ -18,13 +18,13 @@ pub mod unix;
use std::fmt;
use kanidm_proto::v1::{AuthAllowed, AuthMech};
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
pub enum AuthState {
Choose(Vec<AuthMech>),
Continue(Vec<AuthAllowed>),
Denied(String),
Success(String),
Success(String, AuthIssueSession),
}
impl fmt::Debug for AuthState {
@ -33,7 +33,7 @@ impl fmt::Debug for AuthState {
AuthState::Choose(mechs) => write!(f, "AuthState::Choose({:?})", mechs),
AuthState::Continue(allow) => write!(f, "AuthState::Continue({:?})", allow),
AuthState::Denied(reason) => write!(f, "AuthState::Denied({:?})", reason),
AuthState::Success(_token) => write!(f, "AuthState::Success"),
AuthState::Success(_token, issue) => write!(f, "AuthState::Success({:?})", issue),
}
}
}

View file

@ -410,6 +410,21 @@ pub trait IdmServerTransaction<'a> {
}
}
#[instrument(level = "info", skip_all)]
fn validate_and_parse_token_to_uat(
&self,
token: Option<&str>,
ct: Duration,
) -> Result<UserAuthToken, OperationError> {
match self.validate_and_parse_token_to_token(token, ct)? {
Token::UserAuthToken(uat) => Ok(uat),
Token::ApiToken(_apit, _entry) => {
warn!("Unable to process non user auth token");
Err(OperationError::NotAuthenticated)
}
}
}
fn validate_and_parse_token_to_token(
&self,
token: Option<&str>,
@ -878,13 +893,14 @@ impl<'a> IdmServerAuthTransaction<'a> {
//
// Check anything needed? Get the current auth-session-id from request
// because it associates to the nonce's etc which were all cached.
let euuid = self.qs_read.name_to_uuid(init.name.as_str())?;
let euuid = self.qs_read.name_to_uuid(init.username.as_str())?;
// Get the first / single entry we expect here ....
let entry = self.qs_read.internal_search_uuid(&euuid)?;
security_info!(
name = %init.name,
username = %init.username,
issue = ?init.issue,
uuid = %euuid,
"Initiating Authentication Session",
);
@ -956,7 +972,8 @@ impl<'a> IdmServerAuthTransaction<'a> {
};
*/
let (auth_session, state) = AuthSession::new(account, self.webauthn, ct);
let (auth_session, state) =
AuthSession::new(account, init.issue, self.webauthn, ct);
match auth_session {
Some(auth_session) => {
@ -2225,7 +2242,7 @@ mod tests {
use std::time::Duration;
use async_std::task;
use kanidm_proto::v1::{AuthAllowed, AuthMech, AuthType, OperationError};
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, AuthType, OperationError};
use smartstring::alias::String as AttrString;
use uuid::Uuid;
@ -2366,7 +2383,7 @@ mod tests {
debug_assert!(delay.is_none());
match state {
AuthState::Success(_uat) => {
AuthState::Success(_uat, AuthIssueSession::Token) => {
// Check the uat.
}
_ => {
@ -2505,7 +2522,7 @@ mod tests {
debug_assert!(delay.is_none());
match state {
AuthState::Success(token) => {
AuthState::Success(token, AuthIssueSession::Token) => {
// Check the uat.
token
}
@ -2574,7 +2591,7 @@ mod tests {
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Success(_uat) => {
AuthState::Success(_uat, AuthIssueSession::Token) => {
// Check the uat.
}
_ => {
@ -3435,7 +3452,7 @@ mod tests {
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Success(_uat) => {
AuthState::Success(_uat, AuthIssueSession::Token) => {
// Check the uat.
}
_ => {

View file

@ -1,30 +1,47 @@
# Kanidm - Simple and Secure Identity Management
<p align="center">
<img src="https://raw.githubusercontent.com/kanidm/kanidm/master/artwork/logo-small.png" width="20%" height="auto" />
</p>
# Kanidm
## About
Kanidm is an identity management platform written in rust. Our goals are:
Kanidm is a simple and secure identity management platform, which provides services to allow
other systems and application to authenticate against. The project aims for the highest levels
of reliability, security and ease of use.
* Modern identity management platform
* Simple to deploy and integrate with
* Extensible for various needs
* Correct and secure behaviour by default
The goal of this project is to be a complete identity management provider, covering the broadest
possible set of requirements and integrations. You should not need any other components (like Keycloak)
when you use Kanidm. We want to create a project that will be suitable for everything
from personal home deployments, to the largest enterprise needs.
Today the project is still under heavy development to achieve these goals - We have many foundational
parts in place, and many of the required security features, but it is still an Alpha, and should be
treated as such.
To achieve this we rely heavily on strict defaults, simple configuration, and self-healing components.
The project is still growing and some areas are developing at a fast pace. The core of the server
however is reliable and we make all effort to ensure upgrades will always work.
Kanidm supports:
* Oauth2/OIDC Authentication provider for web SSO
* Read only LDAPS gateway
* Linux/Unix integration (with offline authentication)
* SSH key distribution to Linux/Unix systems
* RADIUS for network authentication
* Passkeys / Webauthn for secure cryptographic authentication
* A self service web ui
* Complete CLI tooling for administration
If you want to host your own centralised authentication service, then Kanidm is for you!
## Documentation / Getting Started / Install
If you want to deploy Kanidm to see what it can do, you should read the kanidm book.
If you want to deploy Kanidm to see what it can do, you should read the Kanidm book.
- [Kanidm book (Latest commit)](https://kanidm.github.io/kanidm/master/)
- [Kanidm book (Latest stable)](https://kanidm.github.io/kanidm/stable/)
- [Kanidm book (Latest commit)](https://kanidm.github.io/kanidm/master/)
We also publish limited [support guidelines](https://github.com/kanidm/kanidm/blob/master/project_docs/RELEASE_AND_SUPPORT.md).
We also publish [support guidelines](https://github.com/kanidm/kanidm/blob/master/project_docs/RELEASE_AND_SUPPORT.md)
for what the project will support.
## Code of Conduct / Ethics
@ -42,6 +59,46 @@ answer questions via email, which can be found on their github profile.
[gitter community channel]: https://gitter.im/kanidm/community
## Comparison with other services
### LLDAP
[LLDAP](https://github.com/nitnelave/lldap) is a similar project aiming for a small and easy to administer
LDAP server with a web administration portal. Both projects use the [Kanidm LDAP bindings](https://github.com/kanidm/ldap3), and have
many similar ideas.
The primary benefit of Kanidm over LLDAP is that Kanidm offers a broader set of "built in" features
like Oauth2 and OIDC. To use these from LLDAP you need an external portal like Keycloak, where in Kanidm
they are "built in". However that is also a strength of LLDAP is that is offers "less" which may make
it easier to administer and deploy for you.
If Kanidm is too complex for your needs, you should check out LLDAP as a smaller alternative. If you
want a project which has a broader feature set out of the box, then Kanidm might be a better fit.
### 389-ds / OpenLDAP
Both 389-ds and OpenLDAP are generic LDAP servers. This means they only provide LDAP and you need
to bring your own IDM configuration on top.
If you need the highest levels of customisation possible from your LDAP deployment, then these are
probably better alternatives. If you want a service that is easier to setup and focused on IDM, then
Kanidm is a better choice.
Kanidm was originally inspired by many elements of both 389-ds and OpenLDAP. Already Kanidm is as fast
as (or faster than) 389-ds for performance and scaling.
### FreeIPA
FreeIPA is another identity management service for Linux/Unix, and ships a huge number of features
from LDAP, Kerberos, DNS, Certificate Authority, and more.
FreeIPA however is a complex system, with a huge amount of parts and configuration. This adds a lot
of resource overhead and difficulty for administration.
Kanidm aims to have the features richness of FreeIPA, but without the resource and administration
overheads. If you want a complete IDM package, but in a lighter footprint and easier to manage, then
Kanidm is probably for you.
## Developer Getting Started
If you want to develop on the server, there is a getting started [guide for developers]. IDM
@ -50,50 +107,6 @@ all backgrounds.
[guide for developers]: https://kanidm.github.io/kanidm/master/DEVELOPER_README.html
## Features
### Implemented
* SSH key distribution for servers
* PAM/nsswitch clients (with limited offline auth)
* MFA - TOTP
* Highly concurrent design (MVCC, COW)
* RADIUS integration
* MFA - Webauthn
### Currently Working On
* CLI for administration
* WebUI for self-service with wifi enrollment, claim management and more.
* RBAC/Claims/Policy (limited by time and credential scope)
* OIDC/Oauth
### Upcoming Focus Areas
* Replication (async multiple active write servers, read-only servers)
### Future
* SSH CA management
* Sudo rule distribution via nsswitch
* WebUI for administration
* Account impersonation
* Synchronisation to other IDM services
## Some key project ideas
* All people should be respected and able to be represented securely.
* Devices represent users and their identities - they are part of the authentication.
* Human error occurs - we should be designed to minimise human mistakes and empower people.
* The system should be easy to understand and reason about for users and admins.
### Features We Want to Avoid
* Auditing: This is better solved by SIEM software, so we should generate data they can consume.
* Fully synchronous behaviour: This prevents scaling and our future ability to expand.
* Generic database: We don't want to be another NoSQL database, we want to be an IDM solution.
* Being like LDAP/GSSAPI/Kerberos: These are all legacy protocols that are hard to use and confine our thinking - we should avoid "being like them" or using them as models.
## What does Kanidm mean?
The original project name was rsidm while it was a thought experiment. Now that it's growing

View file

@ -233,7 +233,7 @@ function addBorrowedObject(obj) {
}
function __wbg_adapter_48(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha070437c619effa2(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha86bc8783d36be0a(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
@ -261,11 +261,11 @@ function makeClosure(arg0, arg1, dtor, f) {
return real;
}
function __wbg_adapter_51(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd36b5f2296664138(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h36bbc8108d49feb4(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_54(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h83dbed9e96aca169(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hc4ba360e62a5fa4a(arg0, arg1, addHeapObject(arg2));
}
/**
@ -1037,16 +1037,16 @@ function getImports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5892 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1425, __wbg_adapter_48);
imports.wbg.__wbindgen_closure_wrapper5651 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1343, __wbg_adapter_48);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper6052 = function(arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1460, __wbg_adapter_51);
imports.wbg.__wbindgen_closure_wrapper5814 = function(arg0, arg1, arg2) {
const ret = makeClosure(arg0, arg1, 1378, __wbg_adapter_51);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper6782 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1724, __wbg_adapter_54);
imports.wbg.__wbindgen_closure_wrapper6542 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1635, __wbg_adapter_54);
return addHeapObject(ret);
};

View file

@ -9,7 +9,6 @@ use crate::components::alpha_warning_banner;
use crate::constants::{
CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_DT, CSS_TABLE,
};
use crate::models;
use crate::utils::{do_alert_error, do_page_header, init_request};
use crate::views::AdminRoute;
@ -83,7 +82,7 @@ pub struct AdminListAccountsProps {
/// Pulls all accounts (service or person-class) from the backend and returns a HashMap
/// with the "name" field being the keys, for easy human-facing sortability.
pub async fn get_accounts(token: &str) -> Result<AdminListAccountsMsg, GetError> {
pub async fn get_accounts() -> Result<AdminListAccountsMsg, GetError> {
// TODO: the actual pulling and turning into a BTreeMap in this and get_groups could *probably* be rolled up into one function? The result object differs but all the widgets are the same.
let mut all_accounts = BTreeMap::new();
@ -94,7 +93,7 @@ pub async fn get_accounts(token: &str) -> Result<AdminListAccountsMsg, GetError>
];
for (endpoint, object_type) in endpoints {
let request = init_request(endpoint, token);
let request = init_request(endpoint);
let response = match request.send().await {
Ok(value) => value,
Err(error) => {
@ -146,14 +145,10 @@ impl Component for AdminListAccounts {
fn create(ctx: &Context<Self>) -> Self {
// TODO: work out the querystring thing so we can just show x number of elements
// console::log!("query: {:?}", location().query);
let token = match models::get_bearer_token() {
Some(value) => value,
None => String::from(""),
};
// start pulling the account data on startup
ctx.link().send_future(async move {
match get_accounts(token.clone().as_str()).await {
match get_accounts().await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -327,13 +322,9 @@ impl Component for AdminViewPerson {
type Properties = AdminViewAccountProps;
fn create(ctx: &Context<Self>) -> Self {
let token = match models::get_bearer_token() {
Some(value) => value,
None => String::from(""),
};
let uuid = ctx.props().uuid.clone();
ctx.link().send_future(async move {
match get_person(token.clone().as_str(), &uuid).await {
match get_person(&uuid).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -480,13 +471,9 @@ impl Component for AdminViewServiceAccount {
type Properties = AdminViewAccountProps;
fn create(ctx: &Context<Self>) -> Self {
let token = match models::get_bearer_token() {
Some(value) => value,
None => String::from(""),
};
let uuid = ctx.props().uuid.clone();
ctx.link().send_future(async move {
match get_service_account(token.clone().as_str(), &uuid).await {
match get_service_account(&uuid).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -553,8 +540,8 @@ impl Component for AdminViewServiceAccount {
}
/// pull the details for a single person by UUID
pub async fn get_person(token: &str, uuid: &str) -> Result<AdminViewPersonMsg, GetError> {
let request = init_request(format!("/v1/person/{}", uuid).as_str(), token);
pub async fn get_person(uuid: &str) -> Result<AdminViewPersonMsg, GetError> {
let request = init_request(format!("/v1/person/{}", uuid).as_str());
let response = match request.send().await {
Ok(value) => value,
Err(error) => {
@ -572,11 +559,8 @@ pub async fn get_person(token: &str, uuid: &str) -> Result<AdminViewPersonMsg, G
}
/// pull the details for a single service_account by UUID
pub async fn get_service_account(
token: &str,
uuid: &str,
) -> Result<AdminViewServiceAccountMsg, GetError> {
let request = init_request(format!("/v1/service_account/{}", uuid).as_str(), token);
pub async fn get_service_account(uuid: &str) -> Result<AdminViewServiceAccountMsg, GetError> {
let request = init_request(format!("/v1/service_account/{}", uuid).as_str());
let response = match request.send().await {
Ok(value) => value,
Err(error) => {

View file

@ -7,7 +7,6 @@ use yew_router::prelude::Link;
use crate::components::adminmenu::{Entity, EntityType, GetError};
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_TABLE};
use crate::models;
use crate::utils::{do_alert_error, do_page_header, init_request};
use crate::views::AdminRoute;
@ -64,14 +63,14 @@ pub struct AdminListGroupsProps {
/// Pulls all accounts (service or person-class) from the backend and returns a HashMap
/// with the "name" field being the keys, for easy human-facing sortability.
pub async fn get_groups(token: &str) -> Result<AdminListGroupsMsg, GetError> {
pub async fn get_groups() -> Result<AdminListGroupsMsg, GetError> {
let mut all_groups = BTreeMap::new();
// we iterate over these endpoints
let endpoints = [("/v1/group", EntityType::Group)];
for (endpoint, object_type) in endpoints {
let request = init_request(endpoint, token);
let request = init_request(endpoint);
let response = match request.send().await {
Ok(value) => value,
Err(error) => {
@ -117,14 +116,10 @@ impl Component for AdminListGroups {
fn create(ctx: &Context<Self>) -> Self {
// TODO: work out the querystring thing so we can just show x number of elements
// console::log!("query: {:?}", location().query);
let token = match models::get_bearer_token() {
Some(value) => value,
None => String::from(""),
};
// start pulling the account data on startup
ctx.link().send_future(async move {
match get_groups(token.clone().as_str()).await {
match get_groups().await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -286,14 +281,10 @@ impl Component for AdminViewGroup {
type Properties = AdminViewGroupProps;
fn create(ctx: &Context<Self>) -> Self {
let token = match models::get_bearer_token() {
Some(value) => value,
None => String::from(""),
};
let uuid = ctx.props().uuid.clone();
// TODO: start pulling the group details then send the msg blep blep
ctx.link().send_future(async move {
match get_group(token.clone().as_str(), &uuid).await {
match get_group(&uuid).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -368,8 +359,8 @@ impl Component for AdminViewGroup {
}
/// pull the details for a single group by UUID
pub async fn get_group(token: &str, groupid: &str) -> Result<AdminViewGroupMsg, GetError> {
let request = init_request(format!("/v1/group/{}", groupid).as_str(), token);
pub async fn get_group(groupid: &str) -> Result<AdminViewGroupMsg, GetError> {
let request = init_request(format!("/v1/group/{}", groupid).as_str());
let response = match request.send().await {
Ok(value) => value,
Err(error) => {

View file

@ -7,7 +7,6 @@ use yew_router::prelude::Link;
use crate::components::adminmenu::{Entity, EntityType, GetError};
use crate::components::alpha_warning_banner;
use crate::constants::{CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_TABLE};
use crate::models;
use crate::utils::{do_alert_error, do_page_header, init_request};
use crate::views::AdminRoute;
@ -64,7 +63,7 @@ pub struct AdminListOAuth2Props {
/// Pulls all OAuth2 RPs from the backend and returns a HashMap
/// with the "name" field being the keys, for easy human-facing sortability.
pub async fn get_entities(token: &str) -> Result<AdminListOAuth2Msg, GetError> {
pub async fn get_entities() -> Result<AdminListOAuth2Msg, GetError> {
// TODO: the actual pulling and turning into a BTreeMap across the admin systems could *probably* be rolled up into one function? The result object differs but all the query bits are the same.
let mut oauth2_objects = BTreeMap::new();
@ -72,7 +71,7 @@ pub async fn get_entities(token: &str) -> Result<AdminListOAuth2Msg, GetError> {
let endpoints = [("/v1/oauth2", EntityType::OAuth2RP)];
for (endpoint, object_type) in endpoints {
let request = init_request(endpoint, token);
let request = init_request(endpoint);
let response = match request.send().await {
Ok(value) => value,
Err(error) => {
@ -113,14 +112,10 @@ impl Component for AdminListOAuth2 {
fn create(ctx: &Context<Self>) -> Self {
// TODO: work out the querystring thing so we can just show x number of elements
let token = match models::get_bearer_token() {
Some(value) => value,
None => String::from(""),
};
// start pulling the data on startup
ctx.link().send_future(async move {
match get_entities(token.clone().as_str()).await {
match get_entities().await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -299,15 +294,11 @@ impl Component for AdminViewOAuth2 {
type Properties = AdminViewOAuth2Props;
fn create(ctx: &Context<Self>) -> Self {
let token = match models::get_bearer_token() {
Some(value) => value,
None => String::from(""),
};
let rs_name = ctx.props().rs_name.clone();
// start pulling the data on startup
ctx.link().send_future(async move {
match get_oauth2_rp(token.clone().as_str(), &rs_name).await {
match get_oauth2_rp(&rs_name).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -405,8 +396,8 @@ impl Component for AdminViewOAuth2 {
}
}
pub async fn get_oauth2_rp(token: &str, rs_name: &str) -> Result<AdminViewOAuth2Msg, GetError> {
let request = init_request(format!("/v1/oauth2/{}", rs_name).as_str(), token);
pub async fn get_oauth2_rp(rs_name: &str) -> Result<AdminViewOAuth2Msg, GetError> {
let request = init_request(format!("/v1/oauth2/{}", rs_name).as_str());
let response = match request.send().await {
Ok(value) => value,
Err(error) => {

View file

@ -1,7 +1,5 @@
use std::str::FromStr;
use compact_jwt::{Jws, JwsUnverified};
use kanidm_proto::v1::{SingleStringRequest, UserAuthToken};
use uuid::Uuid;
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{FormData, HtmlFormElement, Request, RequestInit, RequestMode, Response};
@ -69,7 +67,7 @@ pub enum State {
#[derive(PartialEq, Eq, Properties)]
pub struct ChangeUnixPasswordProps {
pub token: String,
pub uat: UserAuthToken,
}
impl Component for ChangeUnixPassword {
@ -98,9 +96,11 @@ impl Component for ChangeUnixPassword {
},
);
}
let tk = ctx.props().token.clone();
ctx.link().send_future(async {
match Self::update_unix_password(tk, fd.password_input).await {
let id = ctx.props().uat.uuid;
ctx.link().send_future(async move {
match Self::update_unix_password(id, fd.password_input).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -257,14 +257,7 @@ impl Component for ChangeUnixPassword {
}
impl ChangeUnixPassword {
async fn update_unix_password(token: String, new_password: String) -> Result<Msg, FetchError> {
let jwtu = JwsUnverified::from_str(&token).expect_throw("Invalid UAT, unable to parse");
let uat: Jws<UserAuthToken> = jwtu
.unsafe_release_without_verification()
.expect_throw("Invalid UAT, unable to release ");
let id = uat.into_inner().uuid.to_string();
async fn update_unix_password(id: Uuid, new_password: String) -> Result<Msg, FetchError> {
let changereq_jsvalue = serde_json::to_string(&SingleStringRequest {
value: new_password,
})
@ -283,10 +276,6 @@ impl ChangeUnixPassword {
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;

View file

@ -1,13 +1,16 @@
// use anyhow::Error;
use gloo::console;
use kanidm_proto::v1::{
AuthAllowed, AuthCredential, AuthMech, AuthRequest, AuthResponse, AuthState, AuthStep,
AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState,
AuthStep,
};
use kanidm_proto::webauthn::PublicKeyCredential;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{CredentialRequestOptions, Request, RequestInit, RequestMode, Response};
use web_sys::{
CredentialRequestOptions, Request, RequestCredentials, RequestInit, RequestMode, Response,
};
use yew::prelude::*;
use yew::virtual_dom::VNode;
use yew_router::prelude::*;
@ -78,7 +81,10 @@ impl From<FetchError> for LoginAppMsg {
impl LoginApp {
async fn auth_init(username: String) -> Result<LoginAppMsg, FetchError> {
let authreq = AuthRequest {
step: AuthStep::Init(username),
step: AuthStep::Init2 {
username,
issue: AuthIssueSession::Cookie,
},
};
let authreq_jsvalue = serde_json::to_string(&authreq)
.map(|s| JsValue::from(&s))
@ -87,6 +93,7 @@ impl LoginApp {
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
opts.body(Some(&authreq_jsvalue));
@ -141,6 +148,7 @@ impl LoginApp {
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
opts.body(Some(&authreq_jsvalue));
@ -542,6 +550,7 @@ impl Component for LoginApp {
#[cfg(debug)]
console::debug!("create".to_string());
// Assume we are here for a good reason.
// -- clear the bearer to prevent conflict
models::clear_bearer_token();
// Do we have a login hint?
let inputvalue = models::pop_login_hint().unwrap_or_else(|| "".to_string());
@ -835,11 +844,22 @@ impl Component for LoginApp {
self.state = LoginState::Denied(reason);
true
}
AuthState::Success(bearer_token) => {
AuthState::Success(_bearer_token) => {
// Store the bearer here!
/*
models::set_bearer_token(bearer_token);
self.state = LoginState::Authenticated;
true
*/
self.state = LoginState::Error {
emsg: "Invalid Issued Session Type, expected cookie".to_string(),
kopid: None,
};
true
}
AuthState::SuccessCookie => {
self.state = LoginState::Authenticated;
true
}
}
}

View file

@ -12,19 +12,6 @@ use yew_router::prelude::{AnyHistory, History};
use crate::manager::Route;
use crate::views::ViewRoute;
pub fn get_bearer_token() -> Option<String> {
let prev_session: Result<String, _> = PersistentStorage::get("kanidm_bearer_token");
#[cfg(debug)]
console::debug!(format!("kanidm_bearer_token -> {:?}", prev_session).as_str());
prev_session.ok()
}
pub fn set_bearer_token(bearer_token: String) {
PersistentStorage::set("kanidm_bearer_token", bearer_token)
.expect_throw("failed to set header");
}
pub fn clear_bearer_token() {
PersistentStorage::delete("kanidm_bearer_token");
}

View file

@ -6,7 +6,7 @@ pub use kanidm_proto::oauth2::{
};
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, RequestRedirect, Response};
use web_sys::{Request, RequestCredentials, RequestInit, RequestMode, RequestRedirect, Response};
use yew::prelude::*;
use yew_router::prelude::*;
@ -15,14 +15,12 @@ use crate::manager::Route;
use crate::{models, utils};
enum State {
// We don't have a token, or something is invalid.
LoginRequired,
// We are in the process of check the auth token to be sure we can proceed.
TokenCheck(String),
TokenCheck,
// Token check done, lets do it.
SubmitAuthReq(String),
SubmitAuthReq,
Consent {
token: String,
client_name: String,
#[allow(dead_code)]
scopes: Vec<String>,
@ -39,6 +37,7 @@ pub struct Oauth2App {
}
pub enum Oauth2Msg {
LoginRequired,
LoginProceed,
ConsentGranted(String),
TokenValid,
@ -68,20 +67,17 @@ impl From<FetchError> for Oauth2Msg {
}
impl Oauth2App {
async fn fetch_token_valid(token: String) -> Result<Oauth2Msg, FetchError> {
async fn fetch_session_valid() -> Result<Oauth2Msg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
let request = Request::new_with_str_and_init("/v1/auth/valid", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
@ -91,7 +87,7 @@ impl Oauth2App {
if status == 200 {
Ok(Oauth2Msg::TokenValid)
} else if status == 401 {
Ok(Oauth2Msg::LoginProceed)
Ok(Oauth2Msg::LoginRequired)
} else {
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
@ -102,10 +98,7 @@ impl Oauth2App {
}
}
async fn fetch_authreq(
token: String,
authreq: AuthorisationRequest,
) -> Result<Oauth2Msg, FetchError> {
async fn fetch_authreq(authreq: AuthorisationRequest) -> Result<Oauth2Msg, FetchError> {
let authreq_jsvalue = serde_json::to_string(&authreq)
.map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise authreq");
@ -113,6 +106,7 @@ impl Oauth2App {
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
opts.body(Some(&authreq_jsvalue));
@ -121,10 +115,6 @@ impl Oauth2App {
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
@ -173,10 +163,7 @@ impl Oauth2App {
}
}
async fn fetch_consent_token(
token: String,
consent_token: String,
) -> Result<Oauth2Msg, FetchError> {
async fn fetch_consent_token(consent_token: String) -> Result<Oauth2Msg, FetchError> {
let consentreq_jsvalue = serde_json::to_string(&consent_token)
.map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise consent_req");
@ -185,6 +172,7 @@ impl Oauth2App {
opts.method("POST");
opts.mode(RequestMode::SameOrigin);
opts.redirect(RequestRedirect::Manual);
opts.credentials(RequestCredentials::SameOrigin);
opts.body(Some(&consentreq_jsvalue));
@ -193,10 +181,6 @@ impl Oauth2App {
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
@ -274,25 +258,17 @@ impl Component for Oauth2App {
// Push the request down. This covers if we move to LoginRequired.
models::push_oauth2_authorisation_request(query);
match models::get_bearer_token() {
Some(token) => {
// Start the fetch req.
// Put the fetch handle into the consent type.
let token_c = token.clone();
ctx.link().send_future(async {
match Self::fetch_token_valid(token_c).await {
match Self::fetch_session_valid().await {
Ok(v) => v,
Err(v) => v.into(),
}
});
Oauth2App {
state: State::TokenCheck(token),
}
}
None => Oauth2App {
state: State::LoginRequired,
},
state: State::TokenCheck,
}
}
@ -307,6 +283,10 @@ impl Component for Oauth2App {
console::debug!("oauth2::update");
match msg {
Oauth2Msg::LoginRequired => {
self.state = State::LoginRequired;
true
}
Oauth2Msg::LoginProceed => {
models::push_return_location(models::Location::Manager(Route::Oauth2));
@ -322,15 +302,14 @@ impl Component for Oauth2App {
let ar = models::pop_oauth2_authorisation_request();
self.state = match (&self.state, ar) {
(State::TokenCheck(token), Some(ar)) => {
let token_c = token.clone();
(State::TokenCheck, Some(ar)) => {
ctx.link().send_future(async {
match Self::fetch_authreq(token_c, ar).await {
match Self::fetch_authreq(ar).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
State::SubmitAuthReq(token.clone())
State::SubmitAuthReq
}
_ => {
console::error!("Invalid state transition");
@ -346,8 +325,7 @@ impl Component for Oauth2App {
consent_token,
} => {
self.state = match &self.state {
State::SubmitAuthReq(token) => State::Consent {
token: token.clone(),
State::SubmitAuthReq => State::Consent {
client_name,
scopes,
pii_scopes,
@ -363,15 +341,13 @@ impl Component for Oauth2App {
Oauth2Msg::ConsentGranted(_) => {
self.state = match &self.state {
State::Consent {
token,
consent_token,
client_name,
..
} => {
let token_c = token.clone();
let cr_c = consent_token.clone();
ctx.link().send_future(async {
match Self::fetch_consent_token(token_c, cr_c).await {
match Self::fetch_consent_token(cr_c).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -453,7 +429,6 @@ impl Component for Oauth2App {
}
}
State::Consent {
token: _,
client_name,
scopes: _,
pii_scopes,
@ -509,7 +484,7 @@ impl Component for Oauth2App {
</div>
}
}
State::SubmitAuthReq(_) | State::TokenCheck(_) => {
State::SubmitAuthReq | State::TokenCheck => {
html! {
<div class="alert alert-light" role="alert">
<h2 class="text-center">{ "Processing ... " }</h2>

View file

@ -3,7 +3,9 @@ use gloo_net::http::Request;
use wasm_bindgen::prelude::*;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
pub use web_sys::InputEvent;
use web_sys::{Document, Event, HtmlElement, HtmlInputElement, RequestMode, Window};
use web_sys::{
Document, Event, HtmlElement, HtmlInputElement, RequestCredentials, RequestMode, Window,
};
use yew::virtual_dom::VNode;
use yew::{html, Html};
@ -85,11 +87,11 @@ pub fn do_footer() -> VNode {
}
/// Builds a request object to a server-local endpoint with the usual requirements
pub fn init_request(endpoint: &str, token: &str) -> gloo_net::http::Request {
pub fn init_request(endpoint: &str) -> gloo_net::http::Request {
Request::new(endpoint)
.mode(RequestMode::SameOrigin)
.credentials(RequestCredentials::SameOrigin)
.header("content-type", "application/json")
.header("authorization", format!("Bearer {}", token).as_str())
}
pub fn do_alert_error(alert_title: &str, alert_message: Option<&str>) -> Html {

View file

@ -1,11 +1,9 @@
use std::str::FromStr;
use compact_jwt::{Jws, JwsUnverified};
use gloo::console;
use kanidm_proto::v1::UserAuthToken;
use serde::{Deserialize, Serialize};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
use web_sys::{Request, RequestCredentials, RequestInit, RequestMode, Response};
use yew::prelude::*;
use yew_router::prelude::*;
@ -72,28 +70,24 @@ enum State {
LoginRequired,
LoggingOut,
Verifying,
Authenticated(String),
Authenticated(UserAuthToken),
Error { emsg: String, kopid: Option<String> },
}
#[derive(PartialEq, Eq, Properties)]
pub struct ViewProps {
pub token: String,
// pub current_user_entry: Option<Entry>,
pub current_user_uat: Option<UserAuthToken>,
pub current_user_uat: UserAuthToken,
}
pub struct ViewsApp {
state: State,
// pub current_user_entry: Option<Entry>,
pub current_user_uat: Option<UserAuthToken>,
}
pub enum ViewsMsg {
Verified(String),
Verified,
ProfileInfoRecieved { uat: UserAuthToken },
Logout,
LogoutComplete,
ProfileInfoRecieved { uat: UserAuthToken },
Error { emsg: String, kopid: Option<String> },
}
@ -117,25 +111,18 @@ impl Component for ViewsApp {
// Ensure the token is valid before we proceed. Could be
// due to a session expiry or something else, but we want to make
// sure we are really authenticated before we proceed.
let state = match models::get_bearer_token() {
Some(token) => {
// Send off the validation event.
ctx.link().send_future(async {
match Self::check_token_valid(token).await {
match Self::check_session_valid().await {
Ok(v) => v,
Err(v) => v.into(),
}
});
State::Verifying
}
None => State::LoginRequired,
};
let state = State::Verifying;
ViewsApp {
state,
current_user_uat: None,
}
ViewsApp { state }
}
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
@ -148,42 +135,34 @@ impl Component for ViewsApp {
#[cfg(debug)]
console::debug!("views::update");
match msg {
ViewsMsg::Verified(token) => {
let tk = token.clone();
self.state = State::Authenticated(token);
// Populate the user profile
ViewsMsg::Verified => {
// Populate the user profile now we know their session is valid.
ctx.link().send_future(async {
match Self::fetch_user_data(tk).await {
match Self::fetch_user_data().await {
Ok(v) => v,
Err(v) => v.into(),
}
});
true
}
ViewsMsg::ProfileInfoRecieved { uat } => {
self.state = State::Authenticated(uat);
true
}
ViewsMsg::Logout => {
match models::get_bearer_token() {
Some(tk) => {
models::clear_bearer_token();
ctx.link().send_future(async {
match Self::fetch_logout(tk).await {
match Self::fetch_logout().await {
Ok(v) => v,
Err(v) => v.into(),
}
});
self.state = State::LoggingOut;
}
None => self.state = State::LoginRequired,
}
true
}
ViewsMsg::LogoutComplete => {
self.state = State::LoginRequired;
true
}
ViewsMsg::ProfileInfoRecieved { uat } => {
self.current_user_uat = Some(uat);
true
}
ViewsMsg::Error { emsg, kopid } => {
self.state = State::Error { emsg, kopid };
true
@ -227,7 +206,7 @@ impl Component for ViewsApp {
</main>
}
}
State::Authenticated(_) => self.view_authenticated(ctx),
State::Authenticated(uat) => self.view_authenticated(ctx, uat),
State::Error { emsg, kopid } => {
html! {
<main class="form-signin">
@ -254,8 +233,8 @@ impl Component for ViewsApp {
impl ViewsApp {
/// The base page for the user dashboard
fn view_authenticated(&self, ctx: &Context<Self>) -> Html {
let current_user_uat = self.current_user_uat.clone();
fn view_authenticated(&self, ctx: &Context<Self>, uat: &UserAuthToken) -> Html {
let current_user_uat = uat.clone();
// WARN set dash-body against body here?
html! {
@ -337,17 +316,13 @@ impl ViewsApp {
<main class="p-3 x-auto">
<Switch<ViewRoute> render={ Switch::render(move |route: &ViewRoute| {
// safety - can't panic because to get to this location we MUST be authenticated!
let token =
models::get_bearer_token().expect_throw("Invalid state, bearer token must be present!");
match route {
ViewRoute::Admin => html!{
<Switch<AdminRoute> render={ Switch::render(admin_routes) } />
},
ViewRoute::Apps => html! { <AppsApp /> },
ViewRoute::Profile => html! { <ProfileApp token={ token } current_user_uat={ current_user_uat.clone() } /> },
ViewRoute::Security => html! { <SecurityApp token={ token } current_user_uat={ current_user_uat.clone() } /> },
ViewRoute::Profile => html! { <ProfileApp current_user_uat={ current_user_uat.clone() } /> },
ViewRoute::Security => html! { <SecurityApp current_user_uat={ current_user_uat.clone() } /> },
ViewRoute::NotFound => html! {
<Redirect<Route> to={Route::NotFound}/>
},
@ -358,10 +333,11 @@ impl ViewsApp {
}
}
async fn check_token_valid(token: String) -> Result<ViewsMsg, FetchError> {
async fn check_session_valid() -> Result<ViewsMsg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
let request = Request::new_with_str_and_init("/v1/auth/valid", &opts)?;
@ -369,10 +345,6 @@ impl ViewsApp {
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
@ -380,9 +352,8 @@ impl ViewsApp {
let status = resp.status();
if status == 200 {
Ok(ViewsMsg::Verified(token))
Ok(ViewsMsg::Verified)
} else if status == 401 {
// Not valid, re-auth
Ok(ViewsMsg::LogoutComplete)
} else {
let headers = resp.headers();
@ -393,25 +364,48 @@ impl ViewsApp {
}
}
async fn fetch_user_data(token: String) -> Result<ViewsMsg, FetchError> {
let jwtu = JwsUnverified::from_str(&token).expect_throw("Invalid UAT, unable to parse");
let uat: Jws<UserAuthToken> = jwtu
.unsafe_release_without_verification()
.expect_throw("Invalid UAT, unable to release");
// We could get rid of this since the token is all we need?
//
// How will we manage this on changes?
Ok(ViewsMsg::ProfileInfoRecieved {
uat: uat.into_inner(),
})
}
async fn fetch_logout(token: String) -> Result<ViewsMsg, FetchError> {
async fn fetch_user_data() -> Result<ViewsMsg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
let request = Request::new_with_str_and_init("/v1/self/_uat", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
if status == 200 {
let jsval = JsFuture::from(resp.json()?).await?;
let uat: UserAuthToken = serde_wasm_bindgen::from_value(jsval)
.map_err(|e| {
let e_msg = format!("serde error -> {:?}", e);
console::error!(e_msg.as_str());
})
.expect_throw("Invalid response type");
Ok(ViewsMsg::ProfileInfoRecieved { uat })
} else {
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
Ok(ViewsMsg::Error { emsg, kopid })
}
}
async fn fetch_logout() -> Result<ViewsMsg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
let request = Request::new_with_str_and_init("/v1/logout", &opts)?;
@ -419,10 +413,6 @@ impl ViewsApp {
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;

View file

@ -1,57 +1,202 @@
use gloo::console;
use wasm_bindgen::UnwrapThrowExt;
use kanidm_proto::v1::{Entry, WhoamiResponse};
use uuid::Uuid;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestCredentials, RequestInit, RequestMode, Response};
use yew::prelude::*;
use yew::virtual_dom::VNode;
use crate::constants::CSS_PAGE_HEADER;
use crate::error::FetchError;
use crate::utils;
use crate::views::ViewProps;
struct Profile {
mail_primary: Option<String>,
spn: String,
displayname: String,
groups: Vec<String>,
uuid: Uuid,
}
impl TryFrom<Entry> for Profile {
type Error = String;
fn try_from(entry: Entry) -> Result<Self, Self::Error> {
console::error!("Entry Dump", format!("{:?}", entry).to_string());
let uuid = entry
.attrs
.get("uuid")
.and_then(|list| list.get(0))
.ok_or_else(|| "Missing UUID".to_string())
.and_then(|uuid_str| {
Uuid::parse_str(uuid_str).map_err(|_| "Invalid UUID".to_string())
})?;
let spn = entry
.attrs
.get("spn")
.and_then(|list| list.get(0))
.cloned()
.ok_or_else(|| "Missing SPN".to_string())?;
let displayname = entry
.attrs
.get("displayname")
.and_then(|list| list.get(0))
.cloned()
.ok_or_else(|| "Missing displayname".to_string())?;
let groups = entry.attrs.get("memberof").cloned().unwrap_or_default();
let mail_primary = entry
.attrs
.get("mail_primary")
.and_then(|list| list.get(0))
.cloned();
Ok(Profile {
mail_primary,
spn,
displayname,
groups,
uuid,
})
}
}
enum State {
Loading,
Ready(Profile),
Error { emsg: String, kopid: Option<String> },
}
pub enum Msg {
Profile { entry: Entry },
Error { emsg: String, kopid: Option<String> },
}
impl From<FetchError> for Msg {
fn from(fe: FetchError) -> Self {
Msg::Error {
emsg: fe.as_string(),
kopid: None,
}
}
}
// User Profile UI
pub struct ProfileApp {}
pub struct ProfileApp {
state: State,
}
impl Component for ProfileApp {
type Message = ();
type Message = Msg;
type Properties = ViewProps;
fn create(_ctx: &Context<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
#[cfg(debug)]
console::debug!("views::profile::create");
ProfileApp {}
ctx.link().send_future(async {
match Self::fetch_profile_data().await {
Ok(v) => v,
Err(v) => v.into(),
}
});
ProfileApp {
state: State::Loading,
}
}
fn changed(&mut self, ctx: &Context<Self>) -> bool {
console::debug!(format!(
"views::profile::changed current_user: {:?}",
ctx.props().current_user_uat,
));
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
true
}
fn update(&mut self, ctx: &Context<Self>, _msg: Self::Message) -> bool {
console::debug!(format!(
"views::profile::update current_user: {:?}",
ctx.props().current_user_uat,
));
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
#[cfg(debug)]
console::debug!("profile::update");
match msg {
Msg::Profile { entry } => {
self.state = match Profile::try_from(entry) {
Ok(profile) => State::Ready(profile),
Err(emsg) => State::Error { emsg, kopid: None },
};
true
}
Msg::Error { emsg, kopid } => {
self.state = State::Error { emsg, kopid };
true
}
}
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
#[cfg(debug)]
console::debug!("views::profile::rendered");
}
/// UI view for the user profile
fn view(&self, ctx: &Context<Self>) -> Html {
let pagecontent = match &ctx.props().current_user_uat {
None => {
match &self.state {
State::Loading => {
html! {
<h2>
{"Loading user info..."}
</h2>
<main class="text-center form-signin h-100">
<div class="vert-center">
<div class="spinner-border text-dark" role="status">
<span class="visually-hidden">{ "Loading..." }</span>
</div>
</div>
</main>
}
}
Some(uat) => {
let mail_primary = match uat.mail_primary.as_ref() {
State::Ready(profile) => self.view_profile(ctx, &profile),
State::Error { emsg, kopid } => self.do_alert_error(
"An error has occured 😔 ",
Some(
format!(
"{}\n\n{}",
emsg.as_str(),
if let Some(opid) = kopid.as_ref() {
format!("Operation ID: {}", opid.clone())
} else {
"Error occurred client-side.".to_string()
}
)
.as_str(),
),
ctx,
),
}
}
}
impl ProfileApp {
fn do_alert_error(
&self,
alert_title: &str,
alert_message: Option<&str>,
_ctx: &Context<Self>,
) -> VNode {
html! {
<div class="container">
<div class="row justify-content-md-center">
<div class="alert alert-danger" role="alert">
<p><strong>{ alert_title }</strong></p>
if let Some(value) = alert_message {
<p>{ value }</p>
}
</div>
</div>
</div>
}
}
/// UI view for the user profile
fn view_profile(&self, _ctx: &Context<Self>, profile: &Profile) -> Html {
let mail_primary = match profile.mail_primary.as_ref() {
Some(email_address) => {
html! {
<a href={ format!("mailto:{}", &email_address)}>
@ -62,21 +207,21 @@ impl Component for ProfileApp {
None => html! { {"<primary email is unset>"}},
};
let spn = &uat.spn.to_owned();
let spn = &profile.spn.to_owned();
let spn_split = spn.split('@');
let username = &spn_split.clone().next().unwrap_throw();
let domain = &spn_split.clone().last().unwrap_throw();
let display_name = uat.displayname.to_owned();
let user_groups: Vec<String> = uat
let display_name = profile.displayname.to_owned();
let user_groups: Vec<String> = profile
.groups
.iter()
.map(|group| {
.map(|group_spn| {
#[allow(clippy::unwrap_used)]
group.spn.split('@').next().unwrap().to_string()
group_spn.split('@').next().unwrap().to_string()
})
.collect();
html! {
let pagecontent = html! {
<dl class="row">
<dt class="col-6">{ "Display Name" }</dt>
<dd class="col">{ display_name }</dd>
@ -118,12 +263,10 @@ impl Component for ProfileApp {
{ "User's UUID" }
</dt>
<dd class="col">
{ format!("{}", &uat.uuid ) }
{ format!("{}", &profile.uuid ) }
</dd>
</dl>
}
}
};
html! {
<>
@ -135,4 +278,43 @@ impl Component for ProfileApp {
</>
}
}
async fn fetch_profile_data() -> Result<Msg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
let request = Request::new_with_str_and_init("/v1/self", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
let status = resp.status();
if status == 200 {
let jsval = JsFuture::from(resp.json()?).await?;
let state: WhoamiResponse = serde_wasm_bindgen::from_value(jsval)
.map_err(|e| {
let e_msg = format!("serde error -> {:?}", e);
console::error!(e_msg.as_str());
})
.expect_throw("Invalid response type");
Ok(Msg::Profile {
entry: state.youare,
})
} else {
let headers = resp.headers();
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
Ok(Msg::Error { emsg, kopid })
}
}
}

View file

@ -1,12 +1,9 @@
use std::str::FromStr;
use compact_jwt::{Jws, JwsUnverified};
#[cfg(debug)]
use gloo::console;
use kanidm_proto::v1::{CUSessionToken, CUStatus, UiHint, UserAuthToken};
use kanidm_proto::v1::{CUSessionToken, CUStatus, UiHint};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
use web_sys::{Request, RequestCredentials, RequestInit, RequestMode, Response};
use yew::prelude::*;
use yew_router::prelude::*;
@ -74,19 +71,12 @@ impl Component for SecurityApp {
Msg::RequestCredentialUpdate => {
// Submit a req to init the session.
// The uuid we want to submit against - hint, it's us.
let token = ctx.props().token.clone();
let jwtu =
JwsUnverified::from_str(&token).expect_throw("Invalid UAT, unable to parse");
let uat: Jws<UserAuthToken> = jwtu
.unsafe_release_without_verification()
.expect_throw("Unvalid UAT, unable to release ");
let id = uat.into_inner().uuid.to_string();
let uat = &ctx.props().current_user_uat;
let id = uat.uuid.to_string();
ctx.link().send_future(async {
match Self::fetch_token_valid(id, token).await {
match Self::fetch_token_valid(id).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -142,7 +132,7 @@ impl Component for SecurityApp {
_ => html! { <></> },
};
let current_user_uat = ctx.props().current_user_uat.clone();
let uat = ctx.props().current_user_uat.clone();
html! {
<>
@ -166,25 +156,24 @@ impl Component for SecurityApp {
</p>
</div>
<hr/>
if let Some(uat) = current_user_uat {
if uat.ui_hints.contains(&UiHint::PosixAccount) {
<div>
<p>
<ChangeUnixPassword token={ctx.props().token.clone()}></ChangeUnixPassword>
<ChangeUnixPassword uat={ uat }/>
</p>
</div>
}
}
</>
}
}
}
impl SecurityApp {
async fn fetch_token_valid(id: String, token: String) -> Result<Msg, FetchError> {
async fn fetch_token_valid(id: String) -> Result<Msg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
let uri = format!("/v1/person/{}/_credential/_update", id);
@ -194,10 +183,6 @@ impl SecurityApp {
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
request
.headers()
.set("authorization", format!("Bearer {}", token).as_str())
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;