mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
20220911 api tokens (#1071)
This commit is contained in:
parent
ad468f0dfa
commit
082464f786
201
Cargo.lock
generated
201
Cargo.lock
generated
|
@ -75,18 +75,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
version = "0.7.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -108,9 +108,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.62"
|
||||
version = "1.0.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305"
|
||||
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
|
||||
|
||||
[[package]]
|
||||
name = "anymap2"
|
||||
|
@ -251,9 +251,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ab006897723d9352f63e2b13047177c3982d8d79709d713ce7747a8f19fd1b0"
|
||||
checksum = "83e21f3a490c72b3b0cf44962180e60045de2925d8dff97918f7ee43c8f637c7"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"concurrent-queue",
|
||||
|
@ -429,7 +429,7 @@ dependencies = [
|
|||
"serde_bytes",
|
||||
"serde_cbor",
|
||||
"serde_json",
|
||||
"sha2 0.10.2",
|
||||
"sha2 0.10.6",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -542,9 +542,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.2"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
|
||||
checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
|
||||
dependencies = [
|
||||
"generic-array 0.14.6",
|
||||
]
|
||||
|
@ -755,12 +755,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "compact_jwt"
|
||||
version = "0.2.4"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9417bb4f581b7a5e08fabb4398b910064363bbfd7b75a10d1da3bfff3ef9b36"
|
||||
checksum = "5656b98b1584764a52906e67caec20dfb9b0179ac2052d0d5937b083bc39a120"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"base64urlsafedata",
|
||||
"hex",
|
||||
"openssl",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -771,16 +772,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "concread"
|
||||
version = "0.3.7"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91896ebca83fd5ac051ee12ab048a4bcfd8c887397127c8f883b77b05288e935"
|
||||
checksum = "6acd004617e219ee07c26b7f6f5f6b4d489c5595e432b87d0bbd8d88db1eebd3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"crossbeam",
|
||||
"crossbeam-epoch",
|
||||
"lru 0.7.8",
|
||||
"smallvec",
|
||||
"sptr",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -890,9 +893,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
|||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813"
|
||||
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -924,7 +927,7 @@ dependencies = [
|
|||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"itertools 0.10.3",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
|
@ -945,7 +948,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools 0.10.3",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1234,11 +1237,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
|
||||
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
|
||||
dependencies = [
|
||||
"block-buffer 0.10.2",
|
||||
"block-buffer 0.10.3",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
|
@ -1376,9 +1379,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fernet"
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93804560e638370a8be6d59ce71ed803e55e230abdbf42598e666b41adda9b1f"
|
||||
checksum = "c6dedfc944f4ac38cac8b74cb1c7b4fb73c175db232d6fa98e9bd1fd81908b89"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"byteorder",
|
||||
|
@ -1838,9 +1841,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086"
|
||||
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
@ -1860,6 +1863,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.10.0"
|
||||
|
@ -1948,9 +1957,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.7.1"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
|
@ -1997,9 +2006,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.46"
|
||||
version = "0.1.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
|
||||
checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
|
@ -2094,9 +2103,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.3"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
@ -2115,9 +2124,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
|
|||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
|
||||
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -2198,6 +2207,7 @@ dependencies = [
|
|||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time 0.2.27",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
|
@ -2365,9 +2375,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.132"
|
||||
version = "0.2.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
|
@ -2459,9 +2469,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
|||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.8"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390"
|
||||
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
|
@ -2558,9 +2568,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
|
||||
checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
@ -2717,7 +2727,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sha2 0.10.2",
|
||||
"sha2 0.10.6",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
@ -2733,9 +2743,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.13.1"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
|
||||
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
|
||||
|
||||
[[package]]
|
||||
name = "oncemutex"
|
||||
|
@ -2975,9 +2985,9 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
|
|||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "716b4eeb6c4a1d3ecc956f75b43ec2e8e8ba80026413e70a3f41fd3313d3492b"
|
||||
checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
|
@ -3182,7 +3192,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.3",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3202,7 +3212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.3",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3216,9 +3226,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.7",
|
||||
]
|
||||
|
@ -3325,9 +3335,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.11"
|
||||
version = "0.11.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92"
|
||||
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"bytes",
|
||||
|
@ -3343,10 +3353,10 @@ dependencies = [
|
|||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite 0.2.9",
|
||||
"proc-macro-hack",
|
||||
|
@ -3571,9 +3581,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -3621,10 +3631,20 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.144"
|
||||
name = "serde_cbor_2"
|
||||
version = "0.12.0-dev"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
|
||||
checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55"
|
||||
dependencies = [
|
||||
"half",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3725,13 +3745,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
|
||||
checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cpufeatures",
|
||||
"digest 0.10.3",
|
||||
"digest 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3833,14 +3853,20 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa"
|
||||
checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sptr"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
|
||||
|
||||
[[package]]
|
||||
name = "sshkeys"
|
||||
version = "0.3.2"
|
||||
|
@ -3939,9 +3965,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.99"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
|
||||
checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -4001,18 +4027,18 @@ checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.32"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994"
|
||||
checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.32"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21"
|
||||
checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -4380,30 +4406,30 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
|
||||
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.21"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
|
||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
|
@ -4677,9 +4703,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "webauthn-authenticator-rs"
|
||||
version = "0.4.5"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f0a0f2b3f25205903b27ecea9012dcff4ad272ee792a41ab46dae6bbabefd4d"
|
||||
checksum = "d30dcdffd0c5dfa110246701399efcc09962c1bb565f61a5d7fe995645ff6f21"
|
||||
dependencies = [
|
||||
"authenticator-ctap2-2021",
|
||||
"base64urlsafedata",
|
||||
|
@ -4687,7 +4713,7 @@ dependencies = [
|
|||
"openssl",
|
||||
"rpassword 5.0.1",
|
||||
"serde",
|
||||
"serde_cbor",
|
||||
"serde_cbor_2",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"url",
|
||||
|
@ -4696,9 +4722,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "webauthn-rs"
|
||||
version = "0.4.5"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b813b9663ddc0b5594b5c54dec399eba428c199a8bb75ed6fde757ec2deca82"
|
||||
checksum = "8d5984278a28dc397c565fd79ee1aba67b74fa365c4eea489f7258b3f902422f"
|
||||
dependencies = [
|
||||
"base64urlsafedata",
|
||||
"serde",
|
||||
|
@ -4710,9 +4736,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "webauthn-rs-core"
|
||||
version = "0.4.5"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68452d453abbd5bb7101fa5c97698940dbdea5cdc7f49a4bea1d546f3dc0f46"
|
||||
checksum = "f6528b4769d8fbe020e0e8c2e66bab6035982a2e9c3f8dac384120718f4763f4"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"base64urlsafedata",
|
||||
|
@ -4722,7 +4748,7 @@ dependencies = [
|
|||
"openssl",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_cbor",
|
||||
"serde_cbor_2",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
|
@ -4734,13 +4760,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "webauthn-rs-proto"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adfdd8694503710db9d9948ea380a3c57fec4371cea6a5a906b2d72caf61838d"
|
||||
checksum = "09e9a265574f8d7b8f8c94c4488bb491a82d7b8e183003a4512d3dceb88efd98"
|
||||
dependencies = [
|
||||
"base64urlsafedata",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.4.3",
|
||||
"serde_json",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
|
@ -4980,7 +5007,7 @@ checksum = "568becce91e872373a4b33f24ddc67e5280ae2536ccb8c9d22a25d398b72c8b0"
|
|||
dependencies = [
|
||||
"derive_builder",
|
||||
"fancy-regex",
|
||||
"itertools 0.10.3",
|
||||
"itertools 0.10.5",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"quick-error",
|
||||
|
|
19
Cargo.toml
19
Cargo.toml
|
@ -25,22 +25,21 @@ exclude = [
|
|||
]
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
[workspace.dependencies]
|
||||
# compact_jwt = { path = "../compact_jwt" }
|
||||
# concread = { path = "../concread" }
|
||||
# concread = { git = "https://github.com/kanidm/concread.git" }
|
||||
|
||||
# idlset = { path = "../idlset" }
|
||||
|
||||
# ldap3_server = { path = "../ldap3_server" }
|
||||
|
||||
|
||||
webauthn-authenticator-rs = "0.4.7"
|
||||
webauthn-rs = "0.4.7"
|
||||
webauthn-rs-core = "0.4.7"
|
||||
webauthn-rs-proto = "0.4.7"
|
||||
# webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" }
|
||||
# webauthn-rs = { path = "../webauthn-rs/webauthn-rs" }
|
||||
# webauthn-rs-core = { path = "../webauthn-rs/webauthn-rs-core" }
|
||||
# webauthn-rs-proto = { path = "../webauthn-rs/webauthn-rs-proto" }
|
||||
# webauthn-authenticator-rs = { path = "../webauthn-rs/webauthn-authenticator-rs" }
|
||||
|
||||
# compact_jwt = { path = "../compact_jwt" }
|
||||
# compact_jwt = { git = "https://github.com/kanidm/compact-jwt.git" }
|
||||
|
||||
|
||||
# enshrinken the WASMs
|
||||
[profile.release.package.kanidmd_web_ui]
|
||||
|
@ -48,7 +47,5 @@ exclude = [
|
|||
codegen-units = 1
|
||||
# optimization for size ( more aggressive )
|
||||
opt-level = 'z'
|
||||
# optimization for size
|
||||
# opt-level = 's'
|
||||
# link time optimization using using whole-program analysis
|
||||
# lto = true
|
||||
|
|
4
iam_migrations/freeipa/00config-mod.ldif
Normal file
4
iam_migrations/freeipa/00config-mod.ldif
Normal file
|
@ -0,0 +1,4 @@
|
|||
dn: cn=Retro Changelog Plugin,cn=plugins,cn=config
|
||||
changetype: modify
|
||||
add: nsslapd-include-suffix
|
||||
nsslapd-include-suffix: cn=accounts,dc=dev,dc=kanidm,dc=com
|
6
iam_migrations/freeipa/01test-sync.sh
Normal file
6
iam_migrations/freeipa/01test-sync.sh
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
ldapsearch -H ldap://localhost -D 'cn=Directory Manager' -w $(cat ipa.pw) -b 'cn=accounts,dc=dev,dc=blackhats,dc=net,dc=au' -x -E \!sync=ro
|
||||
|
||||
|
||||
|
||||
|
|
@ -177,7 +177,52 @@ kanidm service-account create demo_service "Demonstration Service" --name admin
|
|||
kanidm service-account get demo_service --name admin
|
||||
```
|
||||
|
||||
## Resetting Service Account Credentials
|
||||
## Using API Tokens with Service Accounts
|
||||
|
||||
Service accounts can have api tokens generated and associated with them. These tokens can be used for
|
||||
identification of the service account, and for granting extended access rights where the service
|
||||
account may previously have not had the access. Additionally service accounts can have expiry times
|
||||
and other auditing information attached.
|
||||
|
||||
To show api tokens for a service account:
|
||||
|
||||
```shell
|
||||
kanidm service-account api-token status --name admin ACCOUNT_ID
|
||||
kanidm service-account api-token status --name admin demo_service
|
||||
```
|
||||
|
||||
To generate a new api token:
|
||||
|
||||
```shell
|
||||
kanidm service-account api-token generate --name admin ACCOUNT_ID LABEL [EXPIRY]
|
||||
kanidm service-account api-token generate --name admin demo_service "Test Token"
|
||||
kanidm service-account api-token generate --name admin demo_service "Test Token" 2020-09-25T11:22:02+10:00
|
||||
```
|
||||
|
||||
To destroy (revoke) an api token you will need it's token id. This can be shown with the "status"
|
||||
command.
|
||||
|
||||
```shell
|
||||
kanidm service-account api-token destroy --name admin ACCOUNT_ID TOKEN_ID
|
||||
kanidm service-account api-token destroy --name admin demo_service 4de2a4e9-e06a-4c5e-8a1b-33f4e7dd5dc7
|
||||
```
|
||||
|
||||
Api tokens can also be used to gain extended search permissions with LDAP. To do this you can bind
|
||||
with a dn of "" (empty string) and provide the api token in the password.
|
||||
|
||||
```shell
|
||||
ldapwhoami -H ldaps://URL -x -D "" -w "TOKEN"
|
||||
ldapwhoami -H ldaps://idm.example.com -x -D "" -w "..."
|
||||
# u: demo_service@idm.example.com
|
||||
```
|
||||
|
||||
## Resetting Service Account Credentials (Deprecated)
|
||||
|
||||
{{#template
|
||||
templates/kani-warning.md
|
||||
imagepath=images
|
||||
text=Api Tokens are a better method to manage credentials for service accounts, and passwords may be removed in the future!
|
||||
}}
|
||||
|
||||
Service accounts can not have their credentials interactively updated in the same manner as
|
||||
persons. Service accounts may only have server side generated high entropy passwords.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "kanidm_client"
|
||||
version = "1.1.0-alpha.9"
|
||||
authors = ["William Brown <william@blackhats.net.au>"]
|
||||
rust-version = "1.59"
|
||||
rust-version = "1.64"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "Kanidm Client Library"
|
||||
|
@ -16,9 +16,10 @@ reqwest = { version = "^0.11.11", features=["cookies", "json", "native-tls"] }
|
|||
kanidm_proto = { path = "../kanidm_proto", version = "1.1.0-alpha.8" }
|
||||
serde = { version = "^1.0.142", features = ["derive"] }
|
||||
serde_json = "^1.0.83"
|
||||
time = { version = "=0.2.27", features = ["serde", "std"] }
|
||||
tokio = { version = "^1.21.1", features = ["rt", "net", "time", "macros", "sync", "signal"] }
|
||||
toml = "^0.5.9"
|
||||
uuid = { version = "^1.1.2", features = ["serde", "v4"] }
|
||||
url = { version = "^2.3.1", features = ["serde"] }
|
||||
webauthn-rs-proto = { version = "0.4.6", features = ["wasm"] }
|
||||
webauthn-rs-proto = { workspace = true, features = ["wasm"] }
|
||||
|
||||
|
|
|
@ -1168,7 +1168,7 @@ impl KanidmClient {
|
|||
self.perform_get_request("/v1/auth/valid").await
|
||||
}
|
||||
|
||||
pub async fn whoami(&self) -> Result<Option<(Entry, UserAuthToken)>, ClientError> {
|
||||
pub async fn whoami(&self) -> Result<Option<Entry>, ClientError> {
|
||||
let whoami_dest = [self.addr.as_str(), "/v1/self"].concat();
|
||||
// format!("{}/v1/self", self.addr);
|
||||
debug!("{:?}", whoami_dest);
|
||||
|
@ -1211,7 +1211,7 @@ impl KanidmClient {
|
|||
.await
|
||||
.map_err(|e| ClientError::JsonDecode(e, opid))?;
|
||||
|
||||
Ok(Some((r.youare, r.uat)))
|
||||
Ok(Some(r.youare))
|
||||
}
|
||||
|
||||
// Raw DB actions
|
||||
|
|
|
@ -3,7 +3,10 @@ use crate::KanidmClient;
|
|||
use kanidm_proto::v1::AccountUnixExtend;
|
||||
use kanidm_proto::v1::CredentialStatus;
|
||||
use kanidm_proto::v1::Entry;
|
||||
use kanidm_proto::v1::{ApiToken, ApiTokenGenerate};
|
||||
use std::collections::BTreeMap;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
impl KanidmClient {
|
||||
pub async fn idm_service_account_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
|
@ -193,4 +196,45 @@ impl KanidmClient {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn idm_service_account_list_api_token(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Vec<ApiToken>, ClientError> {
|
||||
self.perform_get_request(format!("/v1/service_account/{}/_api_token", id).as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_service_account_generate_api_token(
|
||||
&self,
|
||||
id: &str,
|
||||
label: &str,
|
||||
expiry: Option<OffsetDateTime>,
|
||||
) -> Result<String, ClientError> {
|
||||
let new_token = ApiTokenGenerate {
|
||||
label: label.to_string(),
|
||||
expiry,
|
||||
};
|
||||
self.perform_post_request(
|
||||
format!("/v1/service_account/{}/_api_token", id).as_str(),
|
||||
new_token,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_service_account_destroy_api_token(
|
||||
&self,
|
||||
id: &str,
|
||||
token_id: Uuid,
|
||||
) -> Result<(), ClientError> {
|
||||
self.perform_delete_request(
|
||||
format!(
|
||||
"/v1/service_account/{}/_api_token/{}",
|
||||
id,
|
||||
&token_id.to_string()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "kanidm_proto"
|
||||
version = "1.1.0-alpha.9"
|
||||
authors = ["William Brown <william@blackhats.net.au>"]
|
||||
rust-version = "1.59"
|
||||
rust-version = "1.64"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "Kanidm Protocol Bindings for serde"
|
||||
|
@ -24,7 +24,7 @@ time = { version = "=0.2.27", features = ["serde", "std"] }
|
|||
url = { version = "^2.3.1", features = ["serde"] }
|
||||
urlencoding = "2.1.2"
|
||||
uuid = { version = "^1.1.2", features = ["serde"] }
|
||||
webauthn-rs-proto = "0.4.6"
|
||||
webauthn-rs-proto.workspace = true
|
||||
|
||||
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
||||
last-git-commit = "0.2.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs_proto::{
|
||||
|
@ -238,6 +239,7 @@ pub enum OperationError {
|
|||
ResourceLimit,
|
||||
QueueDisconnected,
|
||||
Webauthn,
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
Wait(time::OffsetDateTime),
|
||||
ReplReplayFailure,
|
||||
ReplEntryNotChanged,
|
||||
|
@ -261,13 +263,13 @@ impl PartialEq for OperationError {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Group {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for Group {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "[ name: {}, ", self.name)?;
|
||||
write!(f, "[ spn: {}, ", self.spn)?;
|
||||
write!(f, "uuid: {} ]", self.uuid)
|
||||
}
|
||||
}
|
||||
|
@ -313,37 +315,36 @@ impl fmt::Display for AuthType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UiHint {
|
||||
PosixAccount,
|
||||
}
|
||||
|
||||
/// The currently authenticated user, and any required metadata for them
|
||||
/// to properly authorise them. This is similar in nature to oauth and the krb
|
||||
/// PAC/PAD structures. Currently we only use this internally, but we should
|
||||
/// consider making it "parseable" by the client so they can have per-session
|
||||
/// group/authorisation data.
|
||||
/// PAC/PAD structures. This information is transparent to clients and CAN
|
||||
/// be parsed by them!
|
||||
///
|
||||
/// This structure and how it works will *very much* change over time from this
|
||||
/// point onward!
|
||||
///
|
||||
/// It's likely that this must have a relationship to the server's user structure
|
||||
/// and to the Entry so that filters or access controls can be applied.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
/// point onward! This means on updates, that sessions will invalidate in many
|
||||
/// cases.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct UserAuthToken {
|
||||
pub session_id: Uuid,
|
||||
pub auth_type: AuthType,
|
||||
// When this token should be considered expired. Interpretation
|
||||
// may depend on the client application.
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub expiry: time::OffsetDateTime,
|
||||
pub uuid: Uuid,
|
||||
pub name: String,
|
||||
pub displayname: String,
|
||||
pub spn: String,
|
||||
pub mail_primary: Option<String>,
|
||||
// pub groups: Vec<Group>,
|
||||
// Should we just retrieve these inside the server instead of in the uat?
|
||||
// or do we want per-session limit capabilities?
|
||||
pub lim_uidx: bool,
|
||||
pub lim_rmax: usize,
|
||||
pub lim_pmax: usize,
|
||||
pub lim_fmax: usize,
|
||||
pub groups: Vec<Group>,
|
||||
pub ui_hints: BTreeSet<UiHint>,
|
||||
}
|
||||
|
||||
impl fmt::Display for UserAuthToken {
|
||||
|
@ -351,11 +352,11 @@ impl fmt::Display for UserAuthToken {
|
|||
// writeln!(f, "name: {}", self.name)?;
|
||||
writeln!(f, "spn: {}", self.spn)?;
|
||||
writeln!(f, "uuid: {}", self.uuid)?;
|
||||
/*
|
||||
writeln!(f, "display: {}", self.displayname)?;
|
||||
for group in &self.groups {
|
||||
writeln!(f, "group: {:?}", group.name)?;
|
||||
writeln!(f, "group: {:?}", group.spn)?;
|
||||
}
|
||||
/*
|
||||
for claim in &self.claims {
|
||||
writeln!(f, "claim: {:?}", claim)?;
|
||||
}
|
||||
|
@ -364,6 +365,65 @@ impl fmt::Display for UserAuthToken {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq for UserAuthToken {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.session_id == other.session_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for UserAuthToken {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ApiToken {
|
||||
// The account this is associated with.
|
||||
pub account_id: Uuid,
|
||||
pub token_id: Uuid,
|
||||
pub label: String,
|
||||
#[serde(with = "time::serde::timestamp::option")]
|
||||
pub expiry: Option<time::OffsetDateTime>,
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub issued_at: time::OffsetDateTime,
|
||||
}
|
||||
|
||||
impl fmt::Display for ApiToken {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "account_id: {}", self.account_id)?;
|
||||
writeln!(f, "token_id: {}", self.token_id)?;
|
||||
writeln!(f, "label: {}", self.label)?;
|
||||
writeln!(f, "issued at: {}", self.issued_at)?;
|
||||
if let Some(expiry) = self.expiry {
|
||||
writeln!(
|
||||
f,
|
||||
"token expiry: {}",
|
||||
expiry
|
||||
.to_offset(
|
||||
time::UtcOffset::try_current_local_offset().unwrap_or(time::UtcOffset::UTC),
|
||||
)
|
||||
.format(time::Format::Rfc3339)
|
||||
)
|
||||
} else {
|
||||
writeln!(f, "token expiry: never")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ApiToken {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.token_id == other.token_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ApiToken {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ApiTokenGenerate {
|
||||
pub label: String,
|
||||
#[serde(with = "time::serde::timestamp::option")]
|
||||
pub expiry: Option<time::OffsetDateTime>,
|
||||
}
|
||||
|
||||
// UAT will need a downcast to Entry, which adds in the claims to the entry
|
||||
// for the purpose of filtering.
|
||||
|
||||
|
@ -981,58 +1041,15 @@ pub struct CUStatus {
|
|||
pub mfaregstate: CURegState,
|
||||
}
|
||||
|
||||
/* Recycle Requests area */
|
||||
|
||||
// Only two actions on recycled is possible. Search and Revive.
|
||||
|
||||
/*
|
||||
pub struct SearchRecycledRequest {
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
||||
impl SearchRecycledRequest {
|
||||
pub fn new(filter: Filter) -> Self {
|
||||
SearchRecycledRequest { filter }
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Need a search response here later.
|
||||
|
||||
/*
|
||||
pub struct ReviveRecycledRequest {
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
||||
impl ReviveRecycledRequest {
|
||||
pub fn new(filter: Filter) -> Self {
|
||||
ReviveRecycledRequest { filter }
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// This doesn't need seralise because it's only accessed via a "get".
|
||||
/*
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WhoamiRequest {}
|
||||
|
||||
impl WhoamiRequest {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct WhoamiResponse {
|
||||
// Should we just embed the entry? Or destructure it?
|
||||
pub youare: Entry,
|
||||
pub uat: UserAuthToken,
|
||||
}
|
||||
|
||||
impl WhoamiResponse {
|
||||
pub fn new(e: Entry, uat: UserAuthToken) -> Self {
|
||||
WhoamiResponse { youare: e, uat }
|
||||
pub fn new(youare: Entry) -> Self {
|
||||
WhoamiResponse { youare }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "kanidm_tools"
|
||||
version = "1.1.0-alpha.9"
|
||||
authors = ["William Brown <william@blackhats.net.au>"]
|
||||
rust-version = "1.59"
|
||||
rust-version = "1.64"
|
||||
edition = "2021"
|
||||
default-run = "kanidm"
|
||||
license = "MPL-2.0"
|
||||
|
@ -47,9 +47,10 @@ tracing-subscriber = { version = "^0.3.14", features = ["env-filter", "fmt"] }
|
|||
tokio = { version = "^1.21.1", features = ["rt", "macros"] }
|
||||
url = { version = "^2.3.1", features = ["serde"] }
|
||||
uuid = "^1.1.2"
|
||||
webauthn-authenticator-rs = { version = "0.4.5", features = ["u2fhid"] }
|
||||
webauthn-authenticator-rs = { workspace = true, features = ["u2fhid"] }
|
||||
zxcvbn = "^2.2.1"
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "^3.2", features = ["derive"] }
|
||||
clap_complete = { version = "^3.2.5"}
|
||||
uuid = "^1.1.2"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
use clap::{CommandFactory, Parser};
|
||||
use clap_complete::{generate_to, Shell};
|
||||
|
|
|
@ -115,7 +115,7 @@ impl CommonOpt {
|
|||
// Is the token (probably) valid?
|
||||
match jwtu
|
||||
.validate_embeded()
|
||||
.map(|jws: Jws<UserAuthToken>| jws.inner)
|
||||
.map(|jws: Jws<UserAuthToken>| jws.into_inner())
|
||||
{
|
||||
Ok(uat) => {
|
||||
if time::OffsetDateTime::now_utc() >= uat.expiry {
|
||||
|
@ -126,8 +126,9 @@ impl CommonOpt {
|
|||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(_e) => {
|
||||
Err(e) => {
|
||||
error!("Unable to read token for requested user - you may need to login again.");
|
||||
debug!(?e, "JWT Error");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
extern crate tracing;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
include!("../opt/kanidm.rs");
|
||||
|
||||
|
@ -43,9 +44,8 @@ impl SelfOpt {
|
|||
match client.whoami().await {
|
||||
Ok(o_ent) => {
|
||||
match o_ent {
|
||||
Some((ent, uat)) => {
|
||||
debug!("{:?}", ent);
|
||||
println!("{}", uat);
|
||||
Some(ent) => {
|
||||
println!("{}", ent);
|
||||
}
|
||||
None => {
|
||||
error!("Authentication with cached token failed, can't query information.");
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{
|
||||
AccountSsh, AccountValidity, ServiceAccountCredential, ServiceAccountOpt, ServiceAccountPosix,
|
||||
AccountSsh, AccountValidity, ServiceAccountApiToken, ServiceAccountCredential,
|
||||
ServiceAccountOpt, ServiceAccountPosix,
|
||||
};
|
||||
use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus};
|
||||
use time::OffsetDateTime;
|
||||
|
@ -11,6 +12,11 @@ impl ServiceAccountOpt {
|
|||
ServiceAccountCredential::Status(apo) => apo.copt.debug,
|
||||
ServiceAccountCredential::GeneratePw(apo) => apo.copt.debug,
|
||||
},
|
||||
ServiceAccountOpt::ApiToken { commands } => match commands {
|
||||
ServiceAccountApiToken::Status(apo) => apo.copt.debug,
|
||||
ServiceAccountApiToken::Generate { copt, .. } => copt.debug,
|
||||
ServiceAccountApiToken::Destroy { copt, .. } => copt.debug,
|
||||
},
|
||||
ServiceAccountOpt::Posix { commands } => match commands {
|
||||
ServiceAccountPosix::Show(apo) => apo.copt.debug,
|
||||
ServiceAccountPosix::Set(apo) => apo.copt.debug,
|
||||
|
@ -61,11 +67,97 @@ impl ServiceAccountOpt {
|
|||
println!("Success: {}", new_pw);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error generating service account credential-> {:?}", e);
|
||||
error!("Error generating service account credential -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}, // End ServiceAccountOpt::Credential
|
||||
ServiceAccountOpt::ApiToken { commands } => match commands {
|
||||
ServiceAccountApiToken::Status(apo) => {
|
||||
let client = apo.copt.to_client().await;
|
||||
match client
|
||||
.idm_service_account_list_api_token(apo.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(tokens) => {
|
||||
if tokens.is_empty() {
|
||||
println!("No api tokens exist");
|
||||
} else {
|
||||
for token in tokens {
|
||||
println!("token: {}", token);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error listing service account api tokens -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceAccountApiToken::Generate {
|
||||
aopts,
|
||||
copt,
|
||||
label,
|
||||
expiry,
|
||||
} => {
|
||||
let expiry_odt = if let Some(t) = expiry {
|
||||
// Convert the time to local timezone.
|
||||
match OffsetDateTime::parse(&t, time::Format::Rfc3339).map(|odt| {
|
||||
odt.to_offset(
|
||||
time::UtcOffset::try_current_local_offset()
|
||||
.unwrap_or(time::UtcOffset::UTC),
|
||||
)
|
||||
}) {
|
||||
Ok(odt) => {
|
||||
debug!("valid until: {}", odt);
|
||||
Some(odt)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let client = copt.to_client().await;
|
||||
|
||||
match client
|
||||
.idm_service_account_generate_api_token(
|
||||
aopts.account_id.as_str(),
|
||||
label,
|
||||
expiry_odt,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(new_token) => {
|
||||
println!("Success: This token will only be displayed ONCE");
|
||||
println!("{}", new_token)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error generating service account api token -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceAccountApiToken::Destroy {
|
||||
aopts,
|
||||
copt,
|
||||
token_id,
|
||||
} => {
|
||||
let client = copt.to_client().await;
|
||||
match client
|
||||
.idm_service_account_destroy_api_token(aopts.account_id.as_str(), *token_id)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
println!("Success");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error destroying service account token -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // End ServiceAccountOpt::ApiToken
|
||||
ServiceAccountOpt::Posix { commands } => match commands {
|
||||
ServiceAccountPosix::Show(aopt) => {
|
||||
let client = aopt.copt.to_client().await;
|
||||
|
|
|
@ -438,7 +438,7 @@ impl SessionOpt {
|
|||
error!(?e, "Unable to verify token signature, may be corrupt");
|
||||
})
|
||||
.map(|jwt| {
|
||||
let uat = jwt.inner;
|
||||
let uat = jwt.into_inner();
|
||||
(u, (t, uat))
|
||||
})
|
||||
.ok()
|
||||
|
|
|
@ -329,14 +329,48 @@ pub enum PersonOpt {
|
|||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ServiceAccountCredential {
|
||||
/// Show the status of this accounts credentials.
|
||||
/// Show the status of this accounts password
|
||||
#[clap(name = "status")]
|
||||
Status(AccountNamedOpt),
|
||||
/// Reset and generate a new service account password. This password can NOT
|
||||
/// be used with the LDAP interface.
|
||||
#[clap(name = "generate-pw")]
|
||||
#[clap(name = "generate")]
|
||||
GeneratePw(AccountNamedOpt),
|
||||
// Future - add a token creator / remover.
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ServiceAccountApiToken {
|
||||
/// Show the status of api tokens associated to this service account.
|
||||
#[clap(name = "status")]
|
||||
Status(AccountNamedOpt),
|
||||
/// Generate a new api token for this service account.
|
||||
#[clap(name = "generate")]
|
||||
Generate {
|
||||
#[clap(flatten)]
|
||||
aopts: AccountCommonOpt,
|
||||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
/// A string describing the token. This is not used to identify the token, it is only
|
||||
/// for human description of the tokens purpose.
|
||||
#[clap(name = "label")]
|
||||
label: String,
|
||||
#[clap(name = "expiry")]
|
||||
/// An optional rfc3339 time of the format "YYYY-MM-DDTHH:MM:SS+TZ", "2020-09-25T11:22:02+10:00".
|
||||
/// After this time the api token will no longer be valid.
|
||||
expiry: Option<String>,
|
||||
},
|
||||
/// Destroy / revoke an api token from this service account. Access to the
|
||||
/// token is NOT required, only the tag/uuid of the token.
|
||||
#[clap(name = "destroy")]
|
||||
Destroy {
|
||||
#[clap(flatten)]
|
||||
aopts: AccountCommonOpt,
|
||||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
/// The UUID of the token to destroy.
|
||||
#[clap(name = "token_id")]
|
||||
token_id: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
|
@ -363,12 +397,18 @@ pub struct ServiceAccountUpdateOpt {
|
|||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ServiceAccountOpt {
|
||||
/// Manage generated passwords or access tokens for this service account.
|
||||
/// Manage the generated password of this service account.
|
||||
#[clap(name = "credential")]
|
||||
Credential {
|
||||
#[clap(subcommand)]
|
||||
commands: ServiceAccountCredential,
|
||||
},
|
||||
/// Manage api tokens associated to this service account.
|
||||
#[clap(name = "api-token")]
|
||||
ApiToken {
|
||||
#[clap(subcommand)]
|
||||
commands: ServiceAccountApiToken,
|
||||
},
|
||||
/// Manage posix extensions for this service account allowing access to unix/linux systems
|
||||
#[clap(name = "posix")]
|
||||
Posix {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "kanidm_unix_int"
|
||||
version = "1.1.0-alpha.9"
|
||||
authors = ["William Brown <william@blackhats.net.au>"]
|
||||
rust-version = "1.59"
|
||||
rust-version = "1.64"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "Kanidm Unix Integration Clients"
|
||||
|
|
|
@ -21,9 +21,9 @@ base64 = "^0.13.0"
|
|||
base64urlsafedata = "0.1.0"
|
||||
chrono = "^0.4.20"
|
||||
compact_jwt = "^0.2.3"
|
||||
concread = "^0.3.7"
|
||||
concread = "^0.4.0"
|
||||
dyn-clone = "^1.0.9"
|
||||
fernet = { version = "^0.1.4", features = ["fernet_danger_timestamps"] }
|
||||
fernet = { version = "^0.2.0", features = ["fernet_danger_timestamps"] }
|
||||
filetime = "^0.2.17"
|
||||
futures = "^0.3.21"
|
||||
futures-util = "^0.3.21"
|
||||
|
@ -61,8 +61,8 @@ url = { version = "^2.3.1", features = ["serde"] }
|
|||
urlencoding = "2.1.2"
|
||||
uuid = { version = "^1.1.2", features = ["serde", "v4" ] }
|
||||
validator = { version = "^0.16.0", features = ["phone"] }
|
||||
webauthn-rs = { version = "0.4.5", features = ["resident-key-support", "preview-features", "danger-credential-internals"] }
|
||||
webauthn-rs-core = "0.4.2-beta.3"
|
||||
webauthn-rs = { workspace = true, features = ["resident-key-support", "preview-features", "danger-credential-internals"] }
|
||||
webauthn-rs-core.workspace = true
|
||||
zxcvbn = "^2.2.1"
|
||||
|
||||
# because windows really can't build without the bundled one
|
||||
|
@ -87,7 +87,7 @@ users = "^0.11.0"
|
|||
[dev-dependencies]
|
||||
criterion = { version = "^0.4.0", features = ["html_reports"] }
|
||||
# For testing webauthn
|
||||
webauthn-authenticator-rs = "0.4.2-beta.3"
|
||||
webauthn-authenticator-rs.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
profiles = { path = "../../profiles" }
|
||||
|
|
|
@ -406,7 +406,7 @@ pub trait AccessControlsTransaction<'a> {
|
|||
#[allow(clippy::mut_from_ref)]
|
||||
fn get_acp_resolve_filter_cache(
|
||||
&self,
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>;
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>;
|
||||
|
||||
fn search_related_acp<'b>(
|
||||
&'b self,
|
||||
|
@ -1303,8 +1303,9 @@ pub struct AccessControlsWriteTransaction<'a> {
|
|||
inner: CowCellWriteTxn<'a, AccessControlsInner>,
|
||||
// acp_related_search_cache_wr: ARCacheWriteTxn<'a, Uuid, Vec<Uuid>>,
|
||||
// acp_related_search_cache: Cell<ARCacheReadTxn<'a, Uuid, Vec<Uuid>>>,
|
||||
acp_resolve_filter_cache:
|
||||
Cell<ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
|
||||
acp_resolve_filter_cache: Cell<
|
||||
ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl<'a> AccessControlsWriteTransaction<'a> {
|
||||
|
@ -1399,7 +1400,7 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsWriteTransaction<'a> {
|
|||
|
||||
fn get_acp_resolve_filter_cache(
|
||||
&self,
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
|
||||
{
|
||||
unsafe {
|
||||
let mptr = self.acp_resolve_filter_cache.as_ptr();
|
||||
|
@ -1408,6 +1409,7 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsWriteTransaction<'a> {
|
|||
'a,
|
||||
(IdentityId, Filter<FilterValid>),
|
||||
Filter<FilterValidResolved>,
|
||||
(),
|
||||
>
|
||||
}
|
||||
}
|
||||
|
@ -1420,8 +1422,9 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsWriteTransaction<'a> {
|
|||
pub struct AccessControlsReadTransaction<'a> {
|
||||
inner: CowCellReadTxn<AccessControlsInner>,
|
||||
// acp_related_search_cache: Cell<ARCacheReadTxn<'a, Uuid, Vec<Uuid>>>,
|
||||
acp_resolve_filter_cache:
|
||||
Cell<ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
|
||||
acp_resolve_filter_cache: Cell<
|
||||
ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>,
|
||||
>,
|
||||
}
|
||||
|
||||
unsafe impl<'a> Sync for AccessControlsReadTransaction<'a> {}
|
||||
|
@ -1456,7 +1459,7 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsReadTransaction<'a> {
|
|||
|
||||
fn get_acp_resolve_filter_cache(
|
||||
&self,
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
|
||||
{
|
||||
unsafe {
|
||||
let mptr = self.acp_resolve_filter_cache.as_ptr();
|
||||
|
@ -1465,6 +1468,7 @@ impl<'a> AccessControlsTransaction<'a> for AccessControlsReadTransaction<'a> {
|
|||
'a,
|
||||
(IdentityId, Filter<FilterValid>),
|
||||
Filter<FilterValidResolved>,
|
||||
(),
|
||||
>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,11 +24,12 @@ use crate::idm::oauth2::{
|
|||
JwkKeySet, Oauth2Error, OidcDiscoveryResponse, OidcToken,
|
||||
};
|
||||
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
||||
use crate::idm::serviceaccount::ListApiTokenEvent;
|
||||
use crate::ldap::{LdapBoundToken, LdapResponseState, LdapServer};
|
||||
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::{
|
||||
AuthRequest, CURequest, CUSessionToken, CUStatus, CredentialStatus, SearchRequest,
|
||||
ApiToken, AuthRequest, CURequest, CUSessionToken, CUStatus, CredentialStatus, SearchRequest,
|
||||
SearchResponse, UnixGroupToken, UnixUserToken, WhoamiResponse,
|
||||
};
|
||||
|
||||
|
@ -102,8 +103,7 @@ impl QueryServerReadV1 {
|
|||
// ! in order to not short-circuit the entire function.
|
||||
let res = spanned!("actors::v1_read::handle<SearchMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(?e, "Invalid identity");
|
||||
e
|
||||
|
@ -329,13 +329,8 @@ impl QueryServerReadV1 {
|
|||
// 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.
|
||||
let (uat, ident) = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| {
|
||||
idms_prox_read
|
||||
.process_uat_to_identity(&uat, ct)
|
||||
.map(|i| (uat, i))
|
||||
})
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(?e, "Invalid identity");
|
||||
e
|
||||
|
@ -353,7 +348,7 @@ impl QueryServerReadV1 {
|
|||
|
||||
match entries.pop() {
|
||||
Some(e) if entries.is_empty() => {
|
||||
WhoamiResult::new(&idms_prox_read.qs_read, &e, uat).map(WhoamiResult::response)
|
||||
WhoamiResult::new(&idms_prox_read.qs_read, &e).map(WhoamiResult::response)
|
||||
}
|
||||
Some(_) => Err(OperationError::InvalidState), // Somehow matched multiple entries...
|
||||
_ => Err(OperationError::NoMatchingEntries),
|
||||
|
@ -379,8 +374,7 @@ impl QueryServerReadV1 {
|
|||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let res = spanned!("actors::v1_read::handle<InternalSearchMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -428,8 +422,7 @@ impl QueryServerReadV1 {
|
|||
|
||||
let res = spanned!("actors::v1_read::handle<InternalSearchRecycledMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -475,8 +468,7 @@ impl QueryServerReadV1 {
|
|||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let res = spanned!("actors::v1_read::handle<InternalRadiusReadMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -541,8 +533,7 @@ impl QueryServerReadV1 {
|
|||
|
||||
let res = spanned!("actors::v1_read::handle<InternalRadiusTokenReadMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -595,8 +586,7 @@ impl QueryServerReadV1 {
|
|||
"actors::v1_read::handle<InternalUnixUserTokenReadMessage>",
|
||||
{
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -649,8 +639,7 @@ impl QueryServerReadV1 {
|
|||
"actors::v1_read::handle<InternalUnixGroupTokenReadMessage>",
|
||||
{
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -701,8 +690,7 @@ impl QueryServerReadV1 {
|
|||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let res = spanned!("actors::v1_read::handle<InternalSshKeyReadMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -769,8 +757,7 @@ impl QueryServerReadV1 {
|
|||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let res = spanned!("actors::v1_read::handle<InternalSshKeyTagReadMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -822,6 +809,39 @@ impl QueryServerReadV1 {
|
|||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "service_account_api_token_get",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_service_account_api_token_get(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
uuid_or_name: String,
|
||||
eventid: Uuid,
|
||||
) -> Result<Vec<ApiToken>, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
let target = idms_prox_read
|
||||
.qs_read
|
||||
.name_to_uuid(uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!("Error resolving id to target");
|
||||
e
|
||||
})?;
|
||||
|
||||
let lte = ListApiTokenEvent { ident, target };
|
||||
|
||||
idms_prox_read.service_account_list_api_token(<e)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "idm_account_unix_auth",
|
||||
|
@ -840,8 +860,7 @@ impl QueryServerReadV1 {
|
|||
// let res = spanned!("actors::v1_read::handle<IdmAccountUnixAuthMessage>", {
|
||||
// resolve the id
|
||||
let ident = idm_auth
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idm_auth.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -893,8 +912,7 @@ impl QueryServerReadV1 {
|
|||
|
||||
let res = spanned!("actors::v1_read::handle<IdmCredentialStatusMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -944,8 +962,7 @@ impl QueryServerReadV1 {
|
|||
|
||||
let res = spanned!("actors::v1_read::handle<IdmBackupCodeViewMessage>", {
|
||||
let ident = idms_prox_read
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_read.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
|
|
@ -24,6 +24,7 @@ use kanidm_proto::v1::OperationError;
|
|||
use crate::filter::{Filter, FilterInvalid};
|
||||
use crate::idm::delayed::DelayedAction;
|
||||
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
||||
use crate::idm::serviceaccount::{DestroyApiTokenEvent, GenerateApiTokenEvent};
|
||||
use crate::utils::duration_from_epoch_now;
|
||||
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
|
@ -34,6 +35,8 @@ use kanidm_proto::v1::{
|
|||
GroupUnixExtend, ModifyRequest,
|
||||
};
|
||||
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct QueryServerWriteV1 {
|
||||
|
@ -72,8 +75,7 @@ impl QueryServerWriteV1 {
|
|||
let ct = duration_from_epoch_now();
|
||||
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -122,8 +124,7 @@ impl QueryServerWriteV1 {
|
|||
let ct = duration_from_epoch_now();
|
||||
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -180,8 +181,7 @@ impl QueryServerWriteV1 {
|
|||
let ct = duration_from_epoch_now();
|
||||
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -222,8 +222,7 @@ impl QueryServerWriteV1 {
|
|||
let res = spanned!("actors::v1_write::handle<ModifyMessage>", {
|
||||
let ct = duration_from_epoch_now();
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -263,8 +262,7 @@ impl QueryServerWriteV1 {
|
|||
let res = spanned!("actors::v1_write::handle<DeleteMessage>", {
|
||||
let ct = duration_from_epoch_now();
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -305,8 +303,7 @@ impl QueryServerWriteV1 {
|
|||
let res = spanned!("actors::v1_write::handle<InternalPatch>", {
|
||||
let ct = duration_from_epoch_now();
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -356,8 +353,7 @@ impl QueryServerWriteV1 {
|
|||
let res = spanned!("actors::v1_write::handle<InternalDeleteMessage>", {
|
||||
let ct = duration_from_epoch_now();
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -396,8 +392,7 @@ impl QueryServerWriteV1 {
|
|||
let res = spanned!("actors::v1_write::handle<ReviveRecycledMessage>", {
|
||||
let ct = duration_from_epoch_now();
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -437,8 +432,7 @@ impl QueryServerWriteV1 {
|
|||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<InternalCredentialSetMessage>", {
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -471,6 +465,90 @@ impl QueryServerWriteV1 {
|
|||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "service_account_credential_generate",
|
||||
skip(self, uat, uuid_or_name, label, expiry, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_service_account_api_token_generate(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
uuid_or_name: String,
|
||||
label: String,
|
||||
expiry: Option<OffsetDateTime>,
|
||||
eventid: Uuid,
|
||||
) -> Result<String, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
})?;
|
||||
|
||||
let target = idms_prox_write
|
||||
.qs_write
|
||||
.name_to_uuid(uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Error resolving id to target");
|
||||
e
|
||||
})?;
|
||||
|
||||
let gte = GenerateApiTokenEvent {
|
||||
ident,
|
||||
target,
|
||||
label,
|
||||
expiry,
|
||||
};
|
||||
|
||||
idms_prox_write
|
||||
.service_account_generate_api_token(>e, ct)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "service_account_credential_generate",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_service_account_api_token_destroy(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
uuid_or_name: String,
|
||||
token_id: Uuid,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
})?;
|
||||
|
||||
let target = idms_prox_write
|
||||
.qs_write
|
||||
.name_to_uuid(uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Error resolving id to target");
|
||||
e
|
||||
})?;
|
||||
|
||||
let dte = DestroyApiTokenEvent {
|
||||
ident,
|
||||
target,
|
||||
token_id,
|
||||
};
|
||||
|
||||
idms_prox_write
|
||||
.service_account_destroy_api_token(&dte)
|
||||
.and_then(|r| idms_prox_write.commit().map(|_| r))
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "idm_credential_update",
|
||||
|
@ -487,8 +565,7 @@ impl QueryServerWriteV1 {
|
|||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdate>", {
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -541,8 +618,7 @@ impl QueryServerWriteV1 {
|
|||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdateIntent>", {
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -680,49 +756,6 @@ impl QueryServerWriteV1 {
|
|||
res
|
||||
}
|
||||
|
||||
/*
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "idm_account_set_password",
|
||||
skip(self, uat, cleartext, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_idmaccountsetpassword(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
cleartext: String,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmAccountSetPasswordMessage>", {
|
||||
idms_prox_write.expire_mfareg_sessions(ct);
|
||||
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
})?;
|
||||
|
||||
let pce = PasswordChangeEvent::from_idm_account_set_password(
|
||||
ident, cleartext,
|
||||
// &idms_prox_write.qs_write,
|
||||
)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Failed to begin idm_account_set_password");
|
||||
e
|
||||
})?;
|
||||
|
||||
idms_prox_write
|
||||
.set_account_password(&pce)
|
||||
.and_then(|_| idms_prox_write.commit())
|
||||
});
|
||||
res
|
||||
}
|
||||
*/
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
name = "handle_service_account_into_person",
|
||||
|
@ -739,8 +772,7 @@ impl QueryServerWriteV1 {
|
|||
let idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmServiceAccountIntoPerson>", {
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -778,8 +810,7 @@ impl QueryServerWriteV1 {
|
|||
"actors::v1_write::handle<InternalRegenerateRadiusMessage>",
|
||||
{
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -832,8 +863,7 @@ impl QueryServerWriteV1 {
|
|||
let idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
spanned!("actors::v1_write::handle<PurgeAttributeMessage>", {
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -888,8 +918,7 @@ impl QueryServerWriteV1 {
|
|||
spanned!("actors::v1_write::handle<RemoveAttributeValuesMessage>", {
|
||||
let ct = duration_from_epoch_now();
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -1107,8 +1136,7 @@ impl QueryServerWriteV1 {
|
|||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmAccountUnixSetCredMessage>", {
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -1163,8 +1191,7 @@ impl QueryServerWriteV1 {
|
|||
let ct = duration_from_epoch_now();
|
||||
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -1229,8 +1256,7 @@ impl QueryServerWriteV1 {
|
|||
let ct = duration_from_epoch_now();
|
||||
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
|
@ -1244,10 +1270,7 @@ impl QueryServerWriteV1 {
|
|||
e
|
||||
})?;
|
||||
|
||||
let ml = ModifyList::new_remove(
|
||||
"oauth2_rs_scope_map",
|
||||
PartialValue::new_oauthscopemap(group_uuid),
|
||||
);
|
||||
let ml = ModifyList::new_remove("oauth2_rs_scope_map", PartialValue::Refer(group_uuid));
|
||||
|
||||
let mdf = match ModifyEvent::from_internal_parts(
|
||||
ident,
|
||||
|
|
|
@ -397,7 +397,7 @@ fn from_vec_dbval1(attr_val: Vec<DbValueV1>) -> Result<DbValueSetV2, OperationEr
|
|||
}
|
||||
// Neither of these should exist yet.
|
||||
Some(DbValueV1::TrustedDeviceEnrollment { u: _ })
|
||||
| Some(DbValueV1::AuthSession { u: _ })
|
||||
| Some(DbValueV1::Session { u: _ })
|
||||
| None => {
|
||||
// Shiiiiii
|
||||
debug_assert!(false);
|
||||
|
|
|
@ -285,6 +285,16 @@ pub struct DbValueCredV1 {
|
|||
pub data: DbCred,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbApiToken {
|
||||
V1 {
|
||||
#[serde(rename = "u")]
|
||||
uuid: Uuid,
|
||||
#[serde(rename = "s")]
|
||||
secret: DbPasswordV1,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValuePasskeyV1 {
|
||||
V4 { u: Uuid, t: String, k: PasskeyV4 },
|
||||
|
@ -341,6 +351,30 @@ pub struct DbValueOauthScopeMapV1 {
|
|||
pub data: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueIdentityId {
|
||||
#[serde(rename = "v1i")]
|
||||
V1Internal,
|
||||
#[serde(rename = "v1u")]
|
||||
V1Uuid(Uuid),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueSession {
|
||||
V1 {
|
||||
#[serde(rename = "u")]
|
||||
refer: Uuid,
|
||||
#[serde(rename = "l")]
|
||||
label: String,
|
||||
#[serde(rename = "e")]
|
||||
expiry: Option<String>,
|
||||
#[serde(rename = "i")]
|
||||
issued_at: String,
|
||||
#[serde(rename = "b")]
|
||||
issued_by: DbValueIdentityId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DbValueV1 {
|
||||
#[serde(rename = "U8")]
|
||||
|
@ -354,7 +388,7 @@ pub enum DbValueV1 {
|
|||
#[serde(rename = "BO")]
|
||||
Bool(bool),
|
||||
#[serde(rename = "SY")]
|
||||
SyntaxType(usize),
|
||||
SyntaxType(u16),
|
||||
#[serde(rename = "IN")]
|
||||
IndexType(usize),
|
||||
#[serde(rename = "RF")]
|
||||
|
@ -403,7 +437,7 @@ pub enum DbValueV1 {
|
|||
#[serde(rename = "TE")]
|
||||
TrustedDeviceEnrollment { u: Uuid },
|
||||
#[serde(rename = "AS")]
|
||||
AuthSession { u: Uuid },
|
||||
Session { u: Uuid },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -419,7 +453,7 @@ pub enum DbValueSetV2 {
|
|||
#[serde(rename = "BO")]
|
||||
Bool(Vec<bool>),
|
||||
#[serde(rename = "SY")]
|
||||
SyntaxType(Vec<usize>),
|
||||
SyntaxType(Vec<u16>),
|
||||
#[serde(rename = "IN")]
|
||||
IndexType(Vec<usize>),
|
||||
#[serde(rename = "RF")]
|
||||
|
@ -469,7 +503,11 @@ pub enum DbValueSetV2 {
|
|||
#[serde(rename = "TE")]
|
||||
TrustedDeviceEnrollment(Vec<Uuid>),
|
||||
#[serde(rename = "AS")]
|
||||
AuthSession(Vec<Uuid>),
|
||||
Session(Vec<DbValueSession>),
|
||||
#[serde(rename = "JE")]
|
||||
JwsKeyEs256(Vec<Vec<u8>>),
|
||||
#[serde(rename = "JR")]
|
||||
JwsKeyRs256(Vec<Vec<u8>>),
|
||||
}
|
||||
|
||||
impl DbValueSetV2 {
|
||||
|
@ -505,7 +543,9 @@ impl DbValueSetV2 {
|
|||
DbValueSetV2::Passkey(set) => set.len(),
|
||||
DbValueSetV2::DeviceKey(set) => set.len(),
|
||||
DbValueSetV2::TrustedDeviceEnrollment(set) => set.len(),
|
||||
DbValueSetV2::AuthSession(set) => set.len(),
|
||||
DbValueSetV2::Session(set) => set.len(),
|
||||
DbValueSetV2::JwsKeyEs256(set) => set.len(),
|
||||
DbValueSetV2::JwsKeyRs256(set) => set.len(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,17 +60,17 @@ pub struct IdlArcSqlite {
|
|||
|
||||
pub struct IdlArcSqliteReadTransaction<'a> {
|
||||
db: IdlSqliteReadTransaction,
|
||||
entry_cache: ARCacheReadTxn<'a, u64, Arc<EntrySealedCommitted>>,
|
||||
idl_cache: ARCacheReadTxn<'a, IdlCacheKey, Box<IDLBitRange>>,
|
||||
name_cache: ARCacheReadTxn<'a, NameCacheKey, NameCacheValue>,
|
||||
entry_cache: ARCacheReadTxn<'a, u64, Arc<EntrySealedCommitted>, ()>,
|
||||
idl_cache: ARCacheReadTxn<'a, IdlCacheKey, Box<IDLBitRange>, ()>,
|
||||
name_cache: ARCacheReadTxn<'a, NameCacheKey, NameCacheValue, ()>,
|
||||
allids: CowCellReadTxn<IDLBitRange>,
|
||||
}
|
||||
|
||||
pub struct IdlArcSqliteWriteTransaction<'a> {
|
||||
db: IdlSqliteWriteTransaction,
|
||||
entry_cache: ARCacheWriteTxn<'a, u64, Arc<EntrySealedCommitted>>,
|
||||
idl_cache: ARCacheWriteTxn<'a, IdlCacheKey, Box<IDLBitRange>>,
|
||||
name_cache: ARCacheWriteTxn<'a, NameCacheKey, NameCacheValue>,
|
||||
entry_cache: ARCacheWriteTxn<'a, u64, Arc<EntrySealedCommitted>, ()>,
|
||||
idl_cache: ARCacheWriteTxn<'a, IdlCacheKey, Box<IDLBitRange>, ()>,
|
||||
name_cache: ARCacheWriteTxn<'a, NameCacheKey, NameCacheValue, ()>,
|
||||
op_ts_max: CowCellWriteTxn<'a, Option<Duration>>,
|
||||
allids: CowCellWriteTxn<'a, IDLBitRange>,
|
||||
maxid: CowCellWriteTxn<'a, u64>,
|
||||
|
|
|
@ -434,7 +434,7 @@ pub const JSON_IDM_ACP_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "mail", "gidnumber", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "mail", "gidnumber", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -456,10 +456,10 @@ pub const JSON_IDM_ACP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -591,7 +591,7 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -613,10 +613,10 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from", "passkeys", "devicekeys", "api_token_session"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
|
|
@ -472,7 +472,7 @@ pub const JSON_SYSTEM_INFO_V1: &str = r#"{
|
|||
"class": ["object", "system_info", "system"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000001"],
|
||||
"description": ["System (local) info and metadata object."],
|
||||
"version": ["7"]
|
||||
"version": ["8"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ pub use crate::constants::system_config::*;
|
|||
pub use crate::constants::uuids::*;
|
||||
|
||||
// Increment this as we add new schema types and values!!!
|
||||
pub const SYSTEM_INDEX_VERSION: i64 = 25;
|
||||
pub const SYSTEM_INDEX_VERSION: i64 = 26;
|
||||
// On test builds, define to 60 seconds
|
||||
#[cfg(test)]
|
||||
pub const PURGE_FREQUENCY: u64 = 60;
|
||||
|
|
|
@ -839,6 +839,37 @@ pub const JSON_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"An es256 private key for jws"
|
||||
],
|
||||
"index": [
|
||||
"EQUALITY"
|
||||
],
|
||||
"unique": [
|
||||
"true"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"jws_es256_private_key"
|
||||
],
|
||||
"syntax": [
|
||||
"JWS_KEY_ES256"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000110"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
|
@ -1047,6 +1078,37 @@ pub const JSON_SCHEMA_ATTR_OAUTH2_PREFER_SHORT_USERNAME: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_API_TOKEN_SESSION: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"A session entry related to an issued api token"
|
||||
],
|
||||
"index": [
|
||||
"EQUALITY"
|
||||
],
|
||||
"unique": [
|
||||
"true"
|
||||
],
|
||||
"multivalue": [
|
||||
"true"
|
||||
],
|
||||
"attributename": [
|
||||
"api_token_session"
|
||||
],
|
||||
"syntax": [
|
||||
"SESSION"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000111"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// === classes ===
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||
|
@ -1220,7 +1282,9 @@ pub const JSON_SCHEMA_CLASS_SERVICE_ACCOUNT: &str = r#"
|
|||
],
|
||||
"systemmay": [
|
||||
"mail",
|
||||
"primary_credential"
|
||||
"primary_credential",
|
||||
"jws_es256_private_key",
|
||||
"api_token_session"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000106"
|
||||
|
|
|
@ -186,6 +186,9 @@ pub const _UUID_SCHEMA_CLASS_DYNGROUP: Uuid = uuid!("00000000-0000-0000-0000-fff
|
|||
pub const _UUID_SCHEMA_ATTR_DYNGROUP_FILTER: Uuid = uuid!("00000000-0000-0000-0000-ffff00000108");
|
||||
pub const _UUID_SCHEMA_ATTR_OAUTH2_PREFERR_SHORT_USERNAME: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000109");
|
||||
pub const _UUID_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000110");
|
||||
pub const _UUID_SCHEMA_ATTR_API_TOKEN_SESSION: Uuid = uuid!("00000000-0000-0000-0000-ffff00000111");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
|
|
@ -33,7 +33,7 @@ use crate::repl::cid::Cid;
|
|||
use crate::repl::entry::EntryChangelog;
|
||||
use crate::schema::{SchemaAttribute, SchemaClass, SchemaTransaction};
|
||||
use crate::value::{IndexType, SyntaxType};
|
||||
use crate::value::{IntentTokenState, PartialValue, Value};
|
||||
use crate::value::{IntentTokenState, PartialValue, Session, Value};
|
||||
use crate::valueset::{self, ValueSet};
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||
|
@ -44,6 +44,7 @@ use crate::be::dbentry::{DbEntry, DbEntryV2, DbEntryVers};
|
|||
use crate::be::dbvalue::DbValueSetV2;
|
||||
use crate::be::{IdxKey, IdxSlope};
|
||||
|
||||
use compact_jwt::JwsSigner;
|
||||
use hashbrown::HashMap;
|
||||
use ldap3_proto::simple::{LdapPartialAttribute, LdapSearchResultEntry};
|
||||
use smartstring::alias::String as AttrString;
|
||||
|
@ -1865,6 +1866,14 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
self.attrs.get(attr).and_then(|vs| vs.as_intenttoken_map())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_ava_as_session_map(
|
||||
&self,
|
||||
attr: &str,
|
||||
) -> Option<&std::collections::BTreeMap<Uuid, Session>> {
|
||||
self.attrs.get(attr).and_then(|vs| vs.as_session_map())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// If possible, return an iterator over the set of values transformed into a `&str`.
|
||||
pub fn get_ava_iter_iname(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
|
||||
|
@ -2031,6 +2040,12 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
.and_then(|vs| vs.to_private_binary_single())
|
||||
}
|
||||
|
||||
pub fn get_ava_single_jws_key_es256(&self, attr: &str) -> Option<&JwsSigner> {
|
||||
self.attrs
|
||||
.get(attr)
|
||||
.and_then(|vs| vs.to_jws_key_es256_single())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Return a single security principle name, if valid to transform this value.
|
||||
pub(crate) fn generate_spn(&self, domain_name: &str) -> Option<Value> {
|
||||
|
|
|
@ -28,7 +28,7 @@ use kanidm_proto::v1::ModifyList as ProtoModifyList;
|
|||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::v1::{
|
||||
AuthCredential, AuthMech, AuthRequest, AuthStep, CreateRequest, DeleteRequest, ModifyRequest,
|
||||
SearchRequest, SearchResponse, UserAuthToken, WhoamiResponse,
|
||||
SearchRequest, SearchResponse, WhoamiResponse,
|
||||
};
|
||||
|
||||
use ldap3_proto::simple::LdapFilter;
|
||||
|
@ -878,25 +878,21 @@ impl AuthResult {
|
|||
|
||||
pub struct WhoamiResult {
|
||||
youare: ProtoEntry,
|
||||
uat: UserAuthToken,
|
||||
}
|
||||
|
||||
impl WhoamiResult {
|
||||
pub fn new(
|
||||
qs: &QueryServerReadTransaction,
|
||||
e: &Entry<EntryReduced, EntryCommitted>,
|
||||
uat: UserAuthToken,
|
||||
) -> Result<Self, OperationError> {
|
||||
Ok(WhoamiResult {
|
||||
youare: e.to_pe(qs)?,
|
||||
uat,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn response(self) -> WhoamiResponse {
|
||||
WhoamiResponse {
|
||||
youare: self.youare,
|
||||
uat: self.uat,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,7 +285,12 @@ impl Filter<FilterValid> {
|
|||
ev: &Identity,
|
||||
idxmeta: Option<&IdxMeta>,
|
||||
mut rsv_cache: Option<
|
||||
&mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>,
|
||||
&mut ARCacheReadTxn<
|
||||
'a,
|
||||
(IdentityId, Filter<FilterValid>),
|
||||
Filter<FilterValidResolved>,
|
||||
(),
|
||||
>,
|
||||
>,
|
||||
) -> Result<Filter<FilterValidResolved>, OperationError> {
|
||||
// Given a filter, resolve Not and SelfUuid to real terms.
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
//! identity may consume during operations to prevent denial-of-service.
|
||||
|
||||
use crate::prelude::*;
|
||||
use kanidm_proto::v1::UserAuthToken;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
use std::hash::Hash;
|
||||
|
@ -20,6 +19,17 @@ pub struct Limits {
|
|||
pub filter_max_elements: usize,
|
||||
}
|
||||
|
||||
impl Default for Limits {
|
||||
fn default() -> Self {
|
||||
Limits {
|
||||
unindexed_allow: false,
|
||||
search_max_results: 128,
|
||||
search_max_filter_test: 256,
|
||||
filter_max_elements: 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Limits {
|
||||
pub fn unlimited() -> Self {
|
||||
Limits {
|
||||
|
@ -29,16 +39,6 @@ impl Limits {
|
|||
filter_max_elements: usize::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
// From a userauthtoken
|
||||
pub fn from_uat(uat: &UserAuthToken) -> Self {
|
||||
Limits {
|
||||
unindexed_allow: uat.lim_uidx,
|
||||
search_max_results: uat.lim_rmax,
|
||||
search_max_filter_test: uat.lim_pmax,
|
||||
filter_max_elements: uat.lim_fmax,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::prelude::*;
|
|||
use crate::schema::SchemaTransaction;
|
||||
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::v1::UiHint;
|
||||
use kanidm_proto::v1::{AuthType, UserAuthToken};
|
||||
use kanidm_proto::v1::{BackupCodesView, CredentialStatus};
|
||||
|
||||
|
@ -26,6 +27,7 @@ use webauthn_rs::prelude::AuthenticationResult;
|
|||
|
||||
lazy_static! {
|
||||
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
|
||||
static ref PVCLASS_POSIXACCOUNT: PartialValue = PartialValue::new_class("posixaccount");
|
||||
}
|
||||
|
||||
macro_rules! try_from_entry {
|
||||
|
@ -93,7 +95,13 @@ macro_rules! try_from_entry {
|
|||
let credential_update_intent_tokens = $value
|
||||
.get_ava_as_intenttokens("credential_update_intent_token")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| BTreeMap::new());
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut ui_hints = BTreeSet::default();
|
||||
|
||||
if $value.attribute_equality("class", &PVCLASS_POSIXACCOUNT) {
|
||||
ui_hints.insert(UiHint::PosixAccount);
|
||||
}
|
||||
|
||||
Ok(Account {
|
||||
uuid,
|
||||
|
@ -107,6 +115,7 @@ macro_rules! try_from_entry {
|
|||
expire,
|
||||
radius_secret,
|
||||
spn,
|
||||
ui_hints,
|
||||
mail_primary,
|
||||
mail,
|
||||
credential_update_intent_tokens,
|
||||
|
@ -135,6 +144,7 @@ pub(crate) struct Account {
|
|||
pub expire: Option<OffsetDateTime>,
|
||||
pub radius_secret: Option<String>,
|
||||
pub spn: String,
|
||||
pub ui_hints: BTreeSet<UiHint>,
|
||||
// TODO #256: When you add mail, you should update the check to zxcvbn
|
||||
// to include these.
|
||||
pub mail_primary: Option<String>,
|
||||
|
@ -206,13 +216,9 @@ impl Account {
|
|||
displayname: self.displayname.clone(),
|
||||
spn: self.spn.clone(),
|
||||
mail_primary: self.mail_primary.clone(),
|
||||
ui_hints: self.ui_hints.clone(),
|
||||
// application: None,
|
||||
// groups: self.groups.iter().map(|g| g.to_proto()).collect(),
|
||||
// What's the best way to get access to these limits with regard to claims/other?
|
||||
lim_uidx: false,
|
||||
lim_rmax: 128,
|
||||
lim_pmax: 256,
|
||||
lim_fmax: 32,
|
||||
groups: self.groups.iter().map(|g| g.to_proto()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -756,7 +756,7 @@ impl AuthSession {
|
|||
.to_userauthtoken(session_id, *time, auth_type)
|
||||
.ok_or(OperationError::InvalidState)?;
|
||||
|
||||
let jwt = Jws { inner: uat };
|
||||
let jwt = Jws::new(uat);
|
||||
|
||||
// Now encrypt and prepare the token for return to the client.
|
||||
let token = jwt
|
||||
|
@ -1353,7 +1353,7 @@ mod tests {
|
|||
.expect("Failed to setup passkey rego challenge");
|
||||
|
||||
let r = wa
|
||||
.do_registration(webauthn.get_origin().clone(), chal)
|
||||
.do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("Failed to create soft passkey");
|
||||
|
||||
let wan_cred = webauthn
|
||||
|
@ -1381,7 +1381,7 @@ mod tests {
|
|||
.expect("Failed to setup passkey rego challenge");
|
||||
|
||||
let r = wa
|
||||
.do_registration(webauthn.get_origin().clone(), chal)
|
||||
.do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("Failed to create soft securitykey");
|
||||
|
||||
let wan_cred = webauthn
|
||||
|
@ -1431,7 +1431,7 @@ mod tests {
|
|||
let (mut session, chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
|
||||
|
||||
let resp = wa
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
|
@ -1460,7 +1460,7 @@ mod tests {
|
|||
|
||||
let resp = wa
|
||||
// HERE -> we use inv_chal instead.
|
||||
.do_authentication(webauthn.get_origin().clone(), inv_chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
|
@ -1484,7 +1484,7 @@ mod tests {
|
|||
.expect("Failed to setup webauthn rego challenge");
|
||||
|
||||
let r = inv_wa
|
||||
.do_registration(webauthn.get_origin().clone(), chal)
|
||||
.do_registration(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("Failed to create soft token");
|
||||
|
||||
let inv_cred = webauthn
|
||||
|
@ -1498,7 +1498,7 @@ mod tests {
|
|||
|
||||
// Create the response.
|
||||
let resp = inv_wa
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("Failed to use softtoken for response.");
|
||||
|
||||
let (mut session, _chal) = start_webauthn_only_session!(&mut audit, account, &webauthn);
|
||||
|
@ -1592,7 +1592,7 @@ mod tests {
|
|||
|
||||
let resp = wa
|
||||
// HERE -> we use inv_chal instead.
|
||||
.do_authentication(webauthn.get_origin().clone(), inv_chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
|
@ -1615,7 +1615,7 @@ mod tests {
|
|||
let chal = chal.unwrap();
|
||||
|
||||
let resp = wa
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
|
@ -1655,7 +1655,7 @@ mod tests {
|
|||
let chal = chal.unwrap();
|
||||
|
||||
let resp = wa
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
|
@ -1771,7 +1771,7 @@ mod tests {
|
|||
|
||||
let resp = wa
|
||||
// HERE -> we use inv_chal instead.
|
||||
.do_authentication(webauthn.get_origin().clone(), inv_chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), inv_chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
|
@ -1794,7 +1794,7 @@ mod tests {
|
|||
let chal = chal.unwrap();
|
||||
|
||||
let resp = wa
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
|
@ -1892,7 +1892,7 @@ mod tests {
|
|||
let chal = chal.unwrap();
|
||||
|
||||
let resp = wa
|
||||
.do_authentication(webauthn.get_origin().clone(), chal)
|
||||
.do_authentication(webauthn.get_allowed_origins()[0].clone(), chal)
|
||||
.expect("failed to use softtoken to authenticate");
|
||||
|
||||
match session.validate_creds(
|
||||
|
|
|
@ -886,7 +886,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
impl<'a> IdmServerCredUpdateTransaction<'a> {
|
||||
#[cfg(test)]
|
||||
pub fn get_origin(&self) -> &Url {
|
||||
self.webauthn.get_origin()
|
||||
&self.webauthn.get_allowed_origins()[0]
|
||||
}
|
||||
|
||||
fn get_current_session(
|
||||
|
@ -962,9 +962,8 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
PasswordQuality::TooShort(PW_MIN_LENGTH)
|
||||
})?;
|
||||
|
||||
// check account pwpolicy (for 3 or 4)? Do we need pw strength beyond this
|
||||
// or should we be enforcing mfa instead
|
||||
if entropy.score() < 3 {
|
||||
// PW's should always be enforced as strong as possible.
|
||||
if entropy.score() < 4 {
|
||||
// The password is too week as per:
|
||||
// https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
|
||||
let feedback: zxcvbn::feedback::Feedback = entropy
|
||||
|
|
|
@ -274,3 +274,13 @@ impl LdapAuthEvent {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LdapTokenAuthEvent {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl LdapTokenAuthEvent {
|
||||
pub fn from_parts(token: String) -> Result<Self, OperationError> {
|
||||
Ok(LdapTokenAuthEvent { token })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,23 +12,29 @@ lazy_static! {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Group {
|
||||
name: String,
|
||||
spn: String,
|
||||
uuid: Uuid,
|
||||
// We'll probably add policy and claims later to this
|
||||
}
|
||||
|
||||
macro_rules! try_from_account_e {
|
||||
($value:expr, $qs:expr) => {{
|
||||
/*
|
||||
let name = $value
|
||||
.get_ava_single_iname("name")
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: name".to_string())
|
||||
})?;
|
||||
*/
|
||||
|
||||
let spn = $value.get_ava_single_proto_string("spn").ok_or(
|
||||
OperationError::InvalidAccountState("Missing attribute: spn".to_string()),
|
||||
)?;
|
||||
|
||||
let uuid = $value.get_uuid();
|
||||
|
||||
let upg = Group { name, uuid };
|
||||
let upg = Group { spn, uuid };
|
||||
|
||||
let mut groups: Vec<Group> = match $value.get_ava_as_refuuid("memberof") {
|
||||
Some(riter) => {
|
||||
|
@ -48,6 +54,7 @@ macro_rules! try_from_account_e {
|
|||
.iter()
|
||||
.map(|e| Group::try_from_entry(e.as_ref()))
|
||||
.collect();
|
||||
|
||||
groups.map_err(|e| {
|
||||
admin_error!(?e, "failed to transform group entries to groups");
|
||||
e
|
||||
|
@ -95,21 +102,29 @@ impl Group {
|
|||
}
|
||||
|
||||
// Now extract our needed attributes
|
||||
/*
|
||||
let name = value
|
||||
.get_ava_single_iname("name")
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: name".to_string())
|
||||
})?;
|
||||
*/
|
||||
let spn =
|
||||
value
|
||||
.get_ava_single_proto_string("spn")
|
||||
.ok_or(OperationError::InvalidAccountState(
|
||||
"Missing attribute: spn".to_string(),
|
||||
))?;
|
||||
|
||||
let uuid = value.get_uuid();
|
||||
|
||||
Ok(Group { name, uuid })
|
||||
Ok(Group { spn, uuid })
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> ProtoGroup {
|
||||
ProtoGroup {
|
||||
name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
uuid: self.uuid.as_hyphenated().to_string(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ pub(crate) mod group;
|
|||
pub mod oauth2;
|
||||
pub(crate) mod radius;
|
||||
pub mod server;
|
||||
pub(crate) mod serviceaccount;
|
||||
pub(crate) mod unix;
|
||||
|
||||
use kanidm_proto::v1::{AuthAllowed, AuthMech};
|
||||
|
|
|
@ -702,7 +702,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
|
||||
// Validate that the session id matches our uat.
|
||||
if consent_req.session_id != uat.session_id {
|
||||
security_info!("consent request sessien id does not match the session id of our UAT.");
|
||||
security_info!("consent request session id does not match the session id of our UAT.");
|
||||
return Err(OperationError::InvalidSessionState);
|
||||
}
|
||||
|
||||
|
@ -988,7 +988,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
trace!(?oidc);
|
||||
|
||||
Some(
|
||||
oidc.sign_with_kid(&o2rs.jws_signer, &client_id)
|
||||
oidc.sign(&o2rs.jws_signer)
|
||||
.map(|jwt_signed| jwt_signed.to_string())
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to encode uat data");
|
||||
|
@ -1310,7 +1310,7 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
})?;
|
||||
|
||||
o2rs.jws_signer
|
||||
.public_key_as_jwk(Some(&o2rs.name))
|
||||
.public_key_as_jwk()
|
||||
.map_err(|e| {
|
||||
admin_error!("Unable to retrieve public key for {} - {:?}", o2rs.name, e);
|
||||
OperationError::InvalidState
|
||||
|
@ -2207,7 +2207,7 @@ mod tests {
|
|||
_ => panic!(),
|
||||
};
|
||||
assert!(use_.unwrap() == JwkUse::Sig);
|
||||
assert!(kid.unwrap() == "test_resource_server")
|
||||
assert!(kid.is_some())
|
||||
}
|
||||
_ => panic!(),
|
||||
};
|
||||
|
@ -2486,7 +2486,7 @@ mod tests {
|
|||
_ => panic!(),
|
||||
};
|
||||
assert!(use_.unwrap() == JwkUse::Sig);
|
||||
assert!(kid.unwrap() == "test_resource_server")
|
||||
assert!(kid.is_some());
|
||||
}
|
||||
_ => panic!(),
|
||||
};
|
||||
|
@ -2742,7 +2742,6 @@ mod tests {
|
|||
)))
|
||||
};
|
||||
|
||||
trace!("ATTACHHERE");
|
||||
assert!(idms_prox_write.qs_write.delete(&de).is_ok());
|
||||
// Assert the consent maps are gone.
|
||||
let ident = idms_prox_write
|
||||
|
|
|
@ -8,9 +8,9 @@ use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
|
|||
#[cfg(test)]
|
||||
use crate::idm::event::PasswordChangeEvent;
|
||||
use crate::idm::event::{
|
||||
CredentialStatusEvent, GeneratePasswordEvent, LdapAuthEvent, RadiusAuthTokenEvent,
|
||||
RegenerateRadiusSecretEvent, UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent,
|
||||
UnixUserTokenEvent,
|
||||
CredentialStatusEvent, GeneratePasswordEvent, LdapAuthEvent, LdapTokenAuthEvent,
|
||||
RadiusAuthTokenEvent, RegenerateRadiusSecretEvent, UnixGroupTokenEvent,
|
||||
UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent,
|
||||
};
|
||||
use crate::idm::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
|
@ -19,9 +19,10 @@ use crate::idm::oauth2::{
|
|||
Oauth2ResourceServersWriteTransaction, OidcDiscoveryResponse, OidcToken,
|
||||
};
|
||||
use crate::idm::radius::RadiusAccount;
|
||||
use crate::idm::serviceaccount::ServiceAccount;
|
||||
use crate::idm::unix::{UnixGroup, UnixUserAccount};
|
||||
use crate::idm::AuthState;
|
||||
use crate::ldap::LdapBoundToken;
|
||||
use crate::ldap::{LdapBoundToken, LdapSession};
|
||||
use crate::prelude::*;
|
||||
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
|
||||
|
||||
|
@ -33,7 +34,7 @@ use crate::idm::delayed::{
|
|||
|
||||
use hashbrown::HashSet;
|
||||
use kanidm_proto::v1::{
|
||||
AuthType, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, UnixGroupToken,
|
||||
ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, UnixGroupToken,
|
||||
UnixUserToken, UserAuthToken,
|
||||
};
|
||||
|
||||
|
@ -400,6 +401,11 @@ impl IdmServerDelayed {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Token {
|
||||
UserAuthToken(UserAuthToken),
|
||||
ApiToken(ApiToken, Arc<EntrySealedCommitted>),
|
||||
}
|
||||
|
||||
pub(crate) trait IdmServerTransaction<'a> {
|
||||
type QsTransactionType: QueryServerTransaction<'a>;
|
||||
|
||||
|
@ -407,6 +413,125 @@ pub(crate) trait IdmServerTransaction<'a> {
|
|||
|
||||
fn get_uat_validator_txn(&self) -> &JwsValidator;
|
||||
|
||||
/// This is the preferred method to transform and securely verify a token into
|
||||
/// an identity that can be used for operations and access enforcement. This
|
||||
/// function *is* aware of the various classes of tokens that may exist, and can
|
||||
/// appropriately check them.
|
||||
///
|
||||
/// The primary method of verification selection is the use of the KID parameter
|
||||
/// that we internally sign with. We can use this to select the appropriate token type
|
||||
/// and validation method.
|
||||
fn validate_and_parse_token_to_ident(
|
||||
&self,
|
||||
token: Option<&str>,
|
||||
ct: Duration,
|
||||
) -> Result<Identity, OperationError> {
|
||||
match self.validate_and_parse_token_to_token(token, ct)? {
|
||||
Token::UserAuthToken(uat) => self.process_uat_to_identity(&uat, ct),
|
||||
Token::ApiToken(apit, entry) => self.process_apit_to_identity(&apit, entry, ct),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_and_parse_token_to_token(
|
||||
&self,
|
||||
token: Option<&str>,
|
||||
ct: Duration,
|
||||
) -> Result<Token, OperationError> {
|
||||
let jwsu = token
|
||||
.ok_or_else(|| {
|
||||
security_info!("No token provided");
|
||||
OperationError::NotAuthenticated
|
||||
})
|
||||
.and_then(|s| {
|
||||
JwsUnverified::from_str(s).map_err(|e| {
|
||||
security_info!(?e, "Unable to decode token");
|
||||
OperationError::NotAuthenticated
|
||||
})
|
||||
})?;
|
||||
|
||||
// Frow the unverified token we can now get the kid, and use that to locate the correct
|
||||
// key to id the token.
|
||||
let jws_validator = self.get_uat_validator_txn();
|
||||
let kid = jwsu.get_jwk_kid().ok_or_else(|| {
|
||||
security_info!("Token does not contain a valid kid");
|
||||
OperationError::NotAuthenticated
|
||||
})?;
|
||||
|
||||
let jwsv_kid = jws_validator.get_jwk_kid().ok_or_else(|| {
|
||||
security_info!("JWS validator does not contain a valid kid");
|
||||
OperationError::NotAuthenticated
|
||||
})?;
|
||||
|
||||
if kid == jwsv_kid {
|
||||
// It's signed by the primary jws, so it's probably a UserAuthToken.
|
||||
let uat = jwsu
|
||||
.validate(jws_validator)
|
||||
.map_err(|e| {
|
||||
security_info!(?e, "Unable to verify token");
|
||||
OperationError::NotAuthenticated
|
||||
})
|
||||
.map(|t: Jws<UserAuthToken>| t.into_inner())?;
|
||||
|
||||
if time::OffsetDateTime::unix_epoch() + ct >= uat.expiry {
|
||||
security_info!("Session expired");
|
||||
Err(OperationError::SessionExpired)
|
||||
} else {
|
||||
Ok(Token::UserAuthToken(uat))
|
||||
}
|
||||
} else {
|
||||
// It's a per-user key, get their validator.
|
||||
let entry = self
|
||||
.get_qs_txn()
|
||||
.internal_search(filter!(f_eq(
|
||||
"jws_es256_private_key",
|
||||
PartialValue::new_iutf8(&kid)
|
||||
)))
|
||||
.and_then(|mut vs| match vs.pop() {
|
||||
Some(entry) if vs.is_empty() => Ok(entry),
|
||||
_ => {
|
||||
admin_error!(
|
||||
?kid,
|
||||
"entries was empty, or matched multiple results for kid"
|
||||
);
|
||||
Err(OperationError::NotAuthenticated)
|
||||
}
|
||||
})?;
|
||||
|
||||
let user_signer = entry
|
||||
.get_ava_single_jws_key_es256("jws_es256_private_key")
|
||||
.ok_or_else(|| {
|
||||
admin_error!(
|
||||
?kid,
|
||||
"A kid was present on entry {} but it does not contain a signing key",
|
||||
entry.get_uuid()
|
||||
);
|
||||
OperationError::NotAuthenticated
|
||||
})?;
|
||||
|
||||
let user_validator = user_signer.get_validator().map_err(|e| {
|
||||
security_info!(?e, "Unable to access token verifier");
|
||||
OperationError::NotAuthenticated
|
||||
})?;
|
||||
|
||||
let apit = jwsu
|
||||
.validate(&user_validator)
|
||||
.map_err(|e| {
|
||||
security_info!(?e, "Unable to verify token");
|
||||
OperationError::NotAuthenticated
|
||||
})
|
||||
.map(|t: Jws<ApiToken>| t.into_inner())?;
|
||||
|
||||
if let Some(expiry) = apit.expiry {
|
||||
if time::OffsetDateTime::unix_epoch() + ct >= expiry {
|
||||
security_info!("Session expired");
|
||||
return Err(OperationError::SessionExpired);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Token::ApiToken(apit, entry))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_and_parse_uat(
|
||||
&self,
|
||||
token: Option<&str>,
|
||||
|
@ -429,7 +554,7 @@ pub(crate) trait IdmServerTransaction<'a> {
|
|||
security_info!(?e, "Unable to verify token");
|
||||
OperationError::NotAuthenticated
|
||||
})
|
||||
.map(|t: Jws<UserAuthToken>| t.inner)
|
||||
.map(|t: Jws<UserAuthToken>| t.into_inner())
|
||||
})?;
|
||||
|
||||
if time::OffsetDateTime::unix_epoch() + ct >= uat.expiry {
|
||||
|
@ -461,6 +586,18 @@ pub(crate) trait IdmServerTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// For any event/operation to proceed, we need to attach an identity to the
|
||||
/// event for security and access processing. When that event is externally
|
||||
/// triggered via one of our various api layers, we process some type of
|
||||
/// account token into this identity. In the current server this is the
|
||||
/// UserAuthToken. For a UserAuthToken to be provided it MUST have been
|
||||
/// cryptographically verified meaning it is now a *trusted* source of
|
||||
/// data that we previously issued.
|
||||
///
|
||||
/// This is the function that is responsible for converting that UAT into
|
||||
/// something we can pin access controls and other limits and references to.
|
||||
/// This is why it is the location where validity windows are checked and other
|
||||
/// relevant session information is injected.
|
||||
fn process_uat_to_identity(
|
||||
&self,
|
||||
uat: &UserAuthToken,
|
||||
|
@ -519,12 +656,87 @@ pub(crate) trait IdmServerTransaction<'a> {
|
|||
|
||||
trace!(claims = ?entry.get_ava_set("claim"), "Applied claims");
|
||||
|
||||
let limits = Limits::from_uat(uat);
|
||||
let limits = Limits::default();
|
||||
Ok(Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
limits,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_apit_to_identity(
|
||||
&self,
|
||||
apit: &ApiToken,
|
||||
entry: Arc<EntrySealedCommitted>,
|
||||
ct: Duration,
|
||||
) -> Result<Identity, OperationError> {
|
||||
let valid = ServiceAccount::check_api_token_valid(ct, apit, &entry);
|
||||
|
||||
if !valid {
|
||||
// Check_api token logs this.
|
||||
return Err(OperationError::SessionExpired);
|
||||
}
|
||||
|
||||
let limits = Limits::default();
|
||||
Ok(Identity {
|
||||
origin: IdentType::User(IdentUser { entry }),
|
||||
limits,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_ldap_session(
|
||||
&self,
|
||||
session: &LdapSession,
|
||||
ct: Duration,
|
||||
) -> Result<Identity, OperationError> {
|
||||
match session {
|
||||
LdapSession::UnixBind(uuid) => {
|
||||
let anon_entry = self
|
||||
.get_qs_txn()
|
||||
.internal_search_uuid(&UUID_ANONYMOUS)
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to validate ldap session -> {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
let entry = if uuid == &UUID_ANONYMOUS {
|
||||
anon_entry.clone()
|
||||
} else {
|
||||
self.get_qs_txn().internal_search_uuid(&uuid).map_err(|e| {
|
||||
admin_error!("Failed to start auth ldap -> {:?}", e);
|
||||
e
|
||||
})?
|
||||
};
|
||||
|
||||
if Account::check_within_valid_time(
|
||||
ct,
|
||||
entry.get_ava_single_datetime("account_valid_from").as_ref(),
|
||||
entry.get_ava_single_datetime("account_expire").as_ref(),
|
||||
) {
|
||||
// Good to go
|
||||
let limits = Limits::default();
|
||||
Ok(Identity {
|
||||
origin: IdentType::User(IdentUser { entry: anon_entry }),
|
||||
limits,
|
||||
})
|
||||
} else {
|
||||
// Nope, expired
|
||||
Err(OperationError::SessionExpired)
|
||||
}
|
||||
}
|
||||
LdapSession::UserAuthToken(uat) => self.process_uat_to_identity(&uat, ct),
|
||||
LdapSession::ApiToken(apit) => {
|
||||
let entry = self
|
||||
.get_qs_txn()
|
||||
.internal_search_uuid(&apit.account_id)
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to validate ldap session -> {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
self.process_apit_to_identity(&apit, entry, ct)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerTransaction<'a> for IdmServerAuthTransaction<'a> {
|
||||
|
@ -921,6 +1133,34 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
res
|
||||
}
|
||||
|
||||
pub async fn token_auth_ldap(
|
||||
&mut self,
|
||||
lae: &LdapTokenAuthEvent,
|
||||
ct: Duration,
|
||||
) -> Result<Option<LdapBoundToken>, OperationError> {
|
||||
match self.validate_and_parse_token_to_token(Some(&lae.token), ct)? {
|
||||
Token::UserAuthToken(uat) => {
|
||||
let spn = uat.spn.clone();
|
||||
Ok(Some(LdapBoundToken {
|
||||
session_id: uat.session_id,
|
||||
spn,
|
||||
effective_session: LdapSession::UserAuthToken(uat),
|
||||
}))
|
||||
}
|
||||
Token::ApiToken(apit, entry) => {
|
||||
let spn = entry.get_ava_single_proto_string("spn").ok_or(
|
||||
OperationError::InvalidAccountState("Missing attribute: spn".to_string()),
|
||||
)?;
|
||||
|
||||
Ok(Some(LdapBoundToken {
|
||||
session_id: apit.token_id,
|
||||
spn,
|
||||
effective_session: LdapSession::ApiToken(apit),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auth_ldap(
|
||||
&mut self,
|
||||
lae: &LdapAuthEvent,
|
||||
|
@ -953,15 +1193,9 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
|
||||
// Account must be anon, so we can gen the uat.
|
||||
Ok(Some(LdapBoundToken {
|
||||
uuid: UUID_ANONYMOUS,
|
||||
effective_uat: account
|
||||
.to_userauthtoken(session_id, ct, AuthType::Anonymous)
|
||||
.ok_or(OperationError::InvalidState)
|
||||
.map_err(|e| {
|
||||
admin_error!("Unable to generate effective_uat -> {:?}", e);
|
||||
e
|
||||
})?,
|
||||
session_id,
|
||||
spn: account.spn,
|
||||
effective_session: LdapSession::UnixBind(UUID_ANONYMOUS),
|
||||
}))
|
||||
} else {
|
||||
let account =
|
||||
|
@ -1015,20 +1249,6 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
.verify_unix_credential(lae.cleartext.as_str(), &self.async_tx, ct)?
|
||||
.is_some()
|
||||
{
|
||||
// Get the anon uat
|
||||
let anon_entry =
|
||||
self.qs_read
|
||||
.internal_search_uuid(&UUID_ANONYMOUS)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
"Failed to find effective uat for auth ldap -> {:?}",
|
||||
e
|
||||
);
|
||||
e
|
||||
})?;
|
||||
let anon_account =
|
||||
Account::try_from_entry_ro(anon_entry.as_ref(), &mut self.qs_read)?;
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
security_info!(
|
||||
"Starting session {} for {} {}",
|
||||
|
@ -1039,14 +1259,8 @@ impl<'a> IdmServerAuthTransaction<'a> {
|
|||
|
||||
Ok(Some(LdapBoundToken {
|
||||
spn: account.spn,
|
||||
uuid: account.uuid,
|
||||
effective_uat: anon_account
|
||||
.to_userauthtoken(session_id, ct, AuthType::UnixPassword)
|
||||
.ok_or(OperationError::InvalidState)
|
||||
.map_err(|e| {
|
||||
admin_error!("Unable to generate effective_uat -> {:?}", e);
|
||||
e
|
||||
})?,
|
||||
session_id,
|
||||
effective_session: LdapSession::UnixBind(account.uuid),
|
||||
}))
|
||||
} else {
|
||||
// PW failure, update softlock.
|
||||
|
@ -1262,7 +1476,7 @@ impl<'a> IdmServerTransaction<'a> for IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||
pub fn get_origin(&self) -> &Url {
|
||||
self.webauthn.get_origin()
|
||||
self.webauthn.get_allowed_origins().get(0).unwrap()
|
||||
}
|
||||
|
||||
fn check_password_quality(
|
||||
|
@ -1288,9 +1502,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
OperationError::PasswordQuality(vec![PasswordFeedback::TooShort(PW_MIN_LENGTH)])
|
||||
})?;
|
||||
|
||||
// check account pwpolicy (for 3 or 4)? Do we need pw strength beyond this
|
||||
// or should we be enforcing mfa instead
|
||||
if entropy.score() < 3 {
|
||||
// Unix PW's are a single factor, so we enforce good pws
|
||||
if entropy.score() < 4 {
|
||||
// The password is too week as per:
|
||||
// https://docs.rs/zxcvbn/2.0.0/zxcvbn/struct.Entropy.html
|
||||
let feedback: zxcvbn::feedback::Feedback = entropy
|
||||
|
@ -1784,7 +1997,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
let modlist = ModifyList::new_list(vec![
|
||||
Modify::Removed(
|
||||
AttrString::from("oauth2_consent_scope_map"),
|
||||
PartialValue::OauthScopeMap(o2cg.oauth2_rs_uuid),
|
||||
PartialValue::Refer(o2cg.oauth2_rs_uuid),
|
||||
),
|
||||
Modify::Present(
|
||||
AttrString::from("oauth2_consent_scope_map"),
|
||||
|
@ -3248,11 +3461,12 @@ mod tests {
|
|||
|
||||
// Check it's valid.
|
||||
idms_prox_read
|
||||
.validate_and_parse_uat(Some(token.as_str()), ct)
|
||||
.validate_and_parse_token_to_ident(Some(token.as_str()), ct)
|
||||
.expect("Failed to validate");
|
||||
|
||||
// In X time it should be INVALID
|
||||
match idms_prox_read.validate_and_parse_uat(Some(token.as_str()), expiry) {
|
||||
match idms_prox_read.validate_and_parse_token_to_ident(Some(token.as_str()), expiry)
|
||||
{
|
||||
Err(OperationError::SessionExpired) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
|
@ -3376,7 +3590,7 @@ mod tests {
|
|||
|
||||
// Check it's valid.
|
||||
idms_prox_read
|
||||
.validate_and_parse_uat(Some(token.as_str()), ct)
|
||||
.validate_and_parse_token_to_ident(Some(token.as_str()), ct)
|
||||
.expect("Failed to validate");
|
||||
|
||||
drop(idms_prox_read);
|
||||
|
@ -3404,11 +3618,11 @@ mod tests {
|
|||
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
assert!(idms_prox_read
|
||||
.validate_and_parse_uat(Some(token.as_str()), ct)
|
||||
.validate_and_parse_token_to_ident(Some(token.as_str()), ct)
|
||||
.is_err());
|
||||
// A new token will work due to the matching key.
|
||||
idms_prox_read
|
||||
.validate_and_parse_uat(Some(new_token.as_str()), ct)
|
||||
.validate_and_parse_token_to_ident(Some(new_token.as_str()), ct)
|
||||
.expect("Failed to validate");
|
||||
}
|
||||
)
|
||||
|
|
455
kanidmd/idm/src/idm/serviceaccount.rs
Normal file
455
kanidmd/idm/src/idm/serviceaccount.rs
Normal file
|
@ -0,0 +1,455 @@
|
|||
use crate::event::SearchEvent;
|
||||
use crate::idm::account::Account;
|
||||
use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
|
||||
use crate::prelude::*;
|
||||
use crate::value::Session;
|
||||
|
||||
use compact_jwt::{Jws, JwsSigner};
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use kanidm_proto::v1::ApiToken;
|
||||
|
||||
// Need to add KID to es256 der for lookups ✅
|
||||
|
||||
// Need to generate the es256 on the account on modifies ✅
|
||||
|
||||
// Add migration to generate the es256 on startup at least once. ✅
|
||||
|
||||
// Create new valueset type to store sessions w_ labels ✅
|
||||
|
||||
// Able to lookup from KID to get service account
|
||||
|
||||
// Able to take token -> ident
|
||||
// -- check still valid
|
||||
|
||||
// revoke
|
||||
|
||||
const GRACE_WINDOW: Duration = Duration::from_secs(600);
|
||||
|
||||
lazy_static! {
|
||||
static ref PVCLASS_SERVICE_ACCOUNT: PartialValue = PartialValue::new_class("service_account");
|
||||
}
|
||||
|
||||
macro_rules! try_from_entry {
|
||||
($value:expr) => {{
|
||||
// Check the classes
|
||||
if !$value.attribute_equality("class", &PVCLASS_SERVICE_ACCOUNT) {
|
||||
return Err(OperationError::InvalidAccountState(
|
||||
"Missing class: service account".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let spn = $value.get_ava_single_proto_string("spn").ok_or(
|
||||
OperationError::InvalidAccountState("Missing attribute: spn".to_string()),
|
||||
)?;
|
||||
|
||||
let jws_key = $value
|
||||
.get_ava_single_jws_key_es256("jws_es256_private_key")
|
||||
.cloned()
|
||||
.ok_or(OperationError::InvalidAccountState(
|
||||
"Missing attribute: jws_es256_private_key".to_string(),
|
||||
))?;
|
||||
|
||||
let api_tokens = $value
|
||||
.get_ava_as_session_map("api_token_session")
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let valid_from = $value.get_ava_single_datetime("account_valid_from");
|
||||
|
||||
let expire = $value.get_ava_single_datetime("account_expire");
|
||||
|
||||
let uuid = $value.get_uuid().clone();
|
||||
|
||||
Ok(ServiceAccount {
|
||||
spn,
|
||||
uuid,
|
||||
valid_from,
|
||||
expire,
|
||||
api_tokens,
|
||||
jws_key,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
pub struct ServiceAccount {
|
||||
pub spn: String,
|
||||
pub uuid: Uuid,
|
||||
|
||||
pub valid_from: Option<OffsetDateTime>,
|
||||
pub expire: Option<OffsetDateTime>,
|
||||
|
||||
pub api_tokens: BTreeMap<Uuid, Session>,
|
||||
|
||||
pub jws_key: JwsSigner,
|
||||
}
|
||||
|
||||
impl ServiceAccount {
|
||||
pub(crate) fn try_from_entry_rw(
|
||||
value: &Entry<EntrySealed, EntryCommitted>,
|
||||
// qs: &mut QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
spanned!("idm::serviceaccount::try_from_entry_rw", {
|
||||
// let groups = Group::try_from_account_entry_rw(value, qs)?;
|
||||
try_from_entry!(value)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn check_api_token_valid(
|
||||
ct: Duration,
|
||||
apit: &ApiToken,
|
||||
entry: &Entry<EntrySealed, EntryCommitted>,
|
||||
) -> bool {
|
||||
let within_valid_window = Account::check_within_valid_time(
|
||||
ct,
|
||||
entry.get_ava_single_datetime("account_valid_from").as_ref(),
|
||||
entry.get_ava_single_datetime("account_expire").as_ref(),
|
||||
);
|
||||
|
||||
if !within_valid_window {
|
||||
security_info!("Account has expired or is not yet valid, not allowing to proceed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the sessions.
|
||||
let session_present = entry
|
||||
.get_ava_as_session_map("api_token_session")
|
||||
.map(|session_map| session_map.get(&apit.token_id).is_some())
|
||||
.unwrap_or(false);
|
||||
|
||||
if session_present {
|
||||
security_info!("A valid session value exists for this token");
|
||||
true
|
||||
} else {
|
||||
let grace = apit.issued_at + GRACE_WINDOW;
|
||||
let current = time::OffsetDateTime::unix_epoch() + ct;
|
||||
trace!(%grace, %current);
|
||||
if current >= grace {
|
||||
security_info!(
|
||||
"The token grace window has passed, and no session exists. Assuming invalid."
|
||||
);
|
||||
false
|
||||
} else {
|
||||
security_info!("The token grace window is in effect. Assuming valid.");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ListApiTokenEvent {
|
||||
// Who initiated this?
|
||||
pub ident: Identity,
|
||||
// Who is it targetting?
|
||||
pub target: Uuid,
|
||||
}
|
||||
|
||||
pub struct GenerateApiTokenEvent {
|
||||
// Who initiated this?
|
||||
pub ident: Identity,
|
||||
// Who is it targetting?
|
||||
pub target: Uuid,
|
||||
// The label
|
||||
pub label: String,
|
||||
// When should it expire?
|
||||
pub expiry: Option<time::OffsetDateTime>,
|
||||
// Limits?
|
||||
}
|
||||
|
||||
impl GenerateApiTokenEvent {
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: Uuid, label: &str, expiry: Option<Duration>) -> Self {
|
||||
GenerateApiTokenEvent {
|
||||
ident: Identity::from_internal(),
|
||||
target,
|
||||
label: label.to_string(),
|
||||
expiry: expiry.map(|ct| time::OffsetDateTime::unix_epoch() + ct),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DestroyApiTokenEvent {
|
||||
// Who initiated this?
|
||||
pub ident: Identity,
|
||||
// Who is it targetting?
|
||||
pub target: Uuid,
|
||||
// Which token id.
|
||||
pub token_id: Uuid,
|
||||
}
|
||||
|
||||
impl DestroyApiTokenEvent {
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: Uuid, token_id: Uuid) -> Self {
|
||||
DestroyApiTokenEvent {
|
||||
ident: Identity::from_internal(),
|
||||
target,
|
||||
token_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||
pub fn service_account_generate_api_token(
|
||||
&self,
|
||||
gte: &GenerateApiTokenEvent,
|
||||
ct: Duration,
|
||||
) -> Result<String, OperationError> {
|
||||
let service_account = self
|
||||
.qs_write
|
||||
.internal_search_uuid(>e.target)
|
||||
.and_then(|account_entry| ServiceAccount::try_from_entry_rw(&account_entry))
|
||||
.map_err(|e| {
|
||||
admin_error!(?e, "Failed to search service account");
|
||||
e
|
||||
})?;
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
let issued_at = time::OffsetDateTime::unix_epoch() + ct;
|
||||
|
||||
// Normalise to UTC incase it was provided as something else.
|
||||
let expiry = gte
|
||||
.expiry
|
||||
.clone()
|
||||
.map(|odt| odt.to_offset(time::UtcOffset::UTC));
|
||||
|
||||
// create a new session
|
||||
let session = Value::Session(
|
||||
session_id,
|
||||
Session {
|
||||
label: gte.label.clone(),
|
||||
expiry,
|
||||
// Need the other inner bits?
|
||||
// for the gracewindow.
|
||||
issued_at,
|
||||
// Who actually created this?
|
||||
issued_by: gte.ident.get_event_origin_id(),
|
||||
},
|
||||
);
|
||||
|
||||
// create the session token (not yet signed)
|
||||
let token = Jws::new(ApiToken {
|
||||
account_id: service_account.uuid,
|
||||
token_id: session_id,
|
||||
label: gte.label.clone(),
|
||||
expiry: gte.expiry.clone(),
|
||||
issued_at,
|
||||
});
|
||||
|
||||
// modify the account to put the session onto it.
|
||||
let modlist = ModifyList::new_list(vec![Modify::Present(
|
||||
AttrString::from("api_token_session"),
|
||||
session,
|
||||
)]);
|
||||
|
||||
self.qs_write
|
||||
.impersonate_modify(
|
||||
// Filter as executed
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuid(gte.target))),
|
||||
// Filter as intended (acp)
|
||||
&filter_all!(f_eq("uuid", PartialValue::new_uuid(gte.target))),
|
||||
&modlist,
|
||||
// Provide the event to impersonate
|
||||
>e.ident,
|
||||
)
|
||||
.and_then(|_| {
|
||||
// The modify succeeded and was allowed, now sign the token for return.
|
||||
token
|
||||
.sign_embed_public_jwk(&service_account.jws_key)
|
||||
.map(|jws_signed| jws_signed.to_string())
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to sign api token");
|
||||
OperationError::CryptographyError
|
||||
})
|
||||
})
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to generate api token {:?}", e);
|
||||
e
|
||||
})
|
||||
// Done!
|
||||
}
|
||||
|
||||
pub fn service_account_destroy_api_token(
|
||||
&self,
|
||||
dte: &DestroyApiTokenEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
// Delete the attribute with uuid.
|
||||
let modlist = ModifyList::new_list(vec![Modify::Removed(
|
||||
AttrString::from("api_token_session"),
|
||||
PartialValue::Refer(dte.token_id),
|
||||
)]);
|
||||
|
||||
self.qs_write
|
||||
.impersonate_modify(
|
||||
// Filter as executed
|
||||
&filter!(f_and!([
|
||||
f_eq("uuid", PartialValue::Uuid(dte.target)),
|
||||
f_eq("api_token_session", PartialValue::Refer(dte.token_id))
|
||||
])),
|
||||
// Filter as intended (acp)
|
||||
&filter_all!(f_and!([
|
||||
f_eq("uuid", PartialValue::Uuid(dte.target)),
|
||||
f_eq("api_token_session", PartialValue::Refer(dte.token_id))
|
||||
])),
|
||||
&modlist,
|
||||
// Provide the event to impersonate
|
||||
&dte.ident,
|
||||
)
|
||||
.map_err(|e| {
|
||||
admin_error!("Failed to destroy api token {:?}", e);
|
||||
e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerProxyReadTransaction<'a> {
|
||||
pub fn service_account_list_api_token(
|
||||
&self,
|
||||
lte: &ListApiTokenEvent,
|
||||
) -> Result<Vec<ApiToken>, OperationError> {
|
||||
// Make an event from the request
|
||||
let srch = match SearchEvent::from_target_uuid_request(
|
||||
lte.ident.clone(),
|
||||
lte.target,
|
||||
&self.qs_read,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
admin_error!("Failed to begin ssh key read: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
match self.qs_read.search_ext(&srch) {
|
||||
Ok(mut entries) => {
|
||||
let r = entries
|
||||
.pop()
|
||||
// get the first entry
|
||||
.and_then(|e| {
|
||||
let account_id = e.get_uuid();
|
||||
// From the entry, turn it into the value
|
||||
e.get_ava_as_session_map("api_token_session").map(|smap| {
|
||||
smap.iter()
|
||||
.map(|(u, s)| ApiToken {
|
||||
account_id,
|
||||
token_id: *u,
|
||||
label: s.label.clone(),
|
||||
expiry: s.expiry.clone(),
|
||||
issued_at: s.issued_at.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
// No matching entry? Return none.
|
||||
Vec::new()
|
||||
});
|
||||
Ok(r)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DestroyApiTokenEvent, GenerateApiTokenEvent, GRACE_WINDOW};
|
||||
use crate::idm::server::IdmServerTransaction;
|
||||
// use crate::prelude::*;
|
||||
|
||||
use crate::event::CreateEvent;
|
||||
use compact_jwt::{Jws, JwsUnverified};
|
||||
use kanidm_proto::v1::ApiToken;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
const TEST_CURRENT_TIME: u64 = 6000;
|
||||
|
||||
#[test]
|
||||
fn test_idm_service_account_api_token() {
|
||||
run_idm_test!(|_qs: &QueryServer,
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed| {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let past_grc = Duration::from_secs(TEST_CURRENT_TIME + 1) + GRACE_WINDOW;
|
||||
let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000);
|
||||
let post_exp = Duration::from_secs(TEST_CURRENT_TIME + 6010);
|
||||
let idms_prox_write = idms.proxy_write(ct);
|
||||
|
||||
let testaccount_uuid = Uuid::new_v4();
|
||||
|
||||
let e1 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("account")),
|
||||
("class", Value::new_class("service_account")),
|
||||
("name", Value::new_iname("test_account_only")),
|
||||
("uuid", Value::new_uuid(testaccount_uuid)),
|
||||
("description", Value::new_utf8s("testaccount")),
|
||||
("displayname", Value::new_utf8s("testaccount"))
|
||||
);
|
||||
|
||||
let ce = CreateEvent::new_internal(vec![e1]);
|
||||
let cr = idms_prox_write.qs_write.create(&ce);
|
||||
assert!(cr.is_ok());
|
||||
|
||||
let gte = GenerateApiTokenEvent::new_internal(testaccount_uuid, "TestToken", Some(exp));
|
||||
|
||||
let api_token = idms_prox_write
|
||||
.service_account_generate_api_token(>e, ct)
|
||||
.expect("failed to generate new api token");
|
||||
|
||||
trace!(?api_token);
|
||||
|
||||
// Deserialise it.
|
||||
let apitoken_unverified =
|
||||
JwsUnverified::from_str(&api_token).expect("Failed to parse apitoken");
|
||||
let apitoken_inner: Jws<ApiToken> = apitoken_unverified
|
||||
.validate_embeded()
|
||||
.expect("Embedded jwk not found");
|
||||
let apitoken_inner = apitoken_inner.into_inner();
|
||||
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_token_to_ident(Some(&api_token), ct)
|
||||
.expect("Unable to verify api token.");
|
||||
|
||||
assert!(ident.get_uuid() == Some(testaccount_uuid));
|
||||
|
||||
// Woohoo! Okay lets test the other edge cases.
|
||||
|
||||
// Check the expiry
|
||||
assert!(
|
||||
idms_prox_write
|
||||
.validate_and_parse_token_to_ident(Some(&api_token), post_exp)
|
||||
.expect_err("Should not succeed")
|
||||
== OperationError::SessionExpired
|
||||
);
|
||||
|
||||
// Delete session
|
||||
let dte = DestroyApiTokenEvent::new_internal(
|
||||
apitoken_inner.account_id,
|
||||
apitoken_inner.token_id,
|
||||
);
|
||||
assert!(idms_prox_write
|
||||
.service_account_destroy_api_token(&dte)
|
||||
.is_ok());
|
||||
|
||||
// Within gracewindow?
|
||||
// This is okay, because we are within the gracewindow.
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_token_to_ident(Some(&api_token), ct)
|
||||
.expect("Unable to verify api token.");
|
||||
assert!(ident.get_uuid() == Some(testaccount_uuid));
|
||||
|
||||
// Past gracewindow?
|
||||
assert!(
|
||||
idms_prox_write
|
||||
.validate_and_parse_token_to_ident(Some(&api_token), past_grc)
|
||||
.expect_err("Should not succeed")
|
||||
== OperationError::SessionExpired
|
||||
);
|
||||
|
||||
assert!(idms_prox_write.commit().is_ok());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@
|
|||
//! are sent to for processing.
|
||||
|
||||
use crate::event::SearchEvent;
|
||||
use crate::idm::event::LdapAuthEvent;
|
||||
use crate::idm::event::{LdapAuthEvent, LdapTokenAuthEvent};
|
||||
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
||||
use crate::prelude::*;
|
||||
use async_std::task;
|
||||
use kanidm_proto::v1::{OperationError, UserAuthToken};
|
||||
use kanidm_proto::v1::{ApiToken, OperationError, UserAuthToken};
|
||||
use ldap3_proto::simple::*;
|
||||
use regex::Regex;
|
||||
use std::collections::BTreeSet;
|
||||
|
@ -26,12 +26,27 @@ pub enum LdapResponseState {
|
|||
BindMultiPartResponse(LdapBoundToken, Vec<LdapMsg>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LdapSession {
|
||||
// Maps through and provides anon read, but allows us to check the validity
|
||||
// of the account still.
|
||||
UnixBind(Uuid),
|
||||
UserAuthToken(UserAuthToken),
|
||||
ApiToken(ApiToken),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LdapBoundToken {
|
||||
// Used to help ID the user doing the action, makes logging nicer.
|
||||
pub spn: String,
|
||||
pub uuid: Uuid,
|
||||
// For now, always anonymous
|
||||
pub effective_uat: UserAuthToken,
|
||||
pub session_id: Uuid,
|
||||
// This is the effective session permission. This is generated from either:
|
||||
// * A valid anonymous bind
|
||||
// * A valid unix pw bind
|
||||
// * A valid ApiToken
|
||||
// In a way, this is a stepping stone to an "ident" but allows us to check
|
||||
// the session is still "valid" depending on it's origin.
|
||||
pub effective_session: LdapSession,
|
||||
}
|
||||
|
||||
pub struct LdapServer {
|
||||
|
@ -269,12 +284,12 @@ impl LdapServer {
|
|||
|
||||
admin_info!(filter = ?lfilter, "LDAP Search Filter");
|
||||
|
||||
// Build the event, with the permissions from effective_uuid
|
||||
// (should always be anonymous at the moment)
|
||||
// Build the event, with the permissions from effective_session
|
||||
//
|
||||
// ! Remember, searchEvent wraps to ignore hidden for us.
|
||||
let se = spanned!("ldap::do_search<core><prepare_se>", {
|
||||
let ident = idm_read
|
||||
.process_uat_to_identity(&uat.effective_uat, ct)
|
||||
.validate_ldap_session(&uat.effective_session, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!("Invalid identity: {:?}", e);
|
||||
e
|
||||
|
@ -346,9 +361,18 @@ impl LdapServer {
|
|||
security_info!("✅ LDAP Bind success anonymous");
|
||||
UUID_ANONYMOUS
|
||||
} else {
|
||||
security_info!("❌ LDAP Bind failure anonymous");
|
||||
// Yeah-nahhhhh
|
||||
return Ok(None);
|
||||
// This is the path to access api-token logins.
|
||||
let lae = LdapTokenAuthEvent::from_parts(pw.to_string())?;
|
||||
return idm_auth.token_auth_ldap(&lae, ct).await.and_then(|r| {
|
||||
idm_auth.commit().map(|_| {
|
||||
if r.is_some() {
|
||||
security_info!(%dn, "✅ LDAP Bind success");
|
||||
} else {
|
||||
security_info!(%dn, "❌ LDAP Bind failure");
|
||||
};
|
||||
r
|
||||
})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let rdn = match self
|
||||
|
@ -528,11 +552,16 @@ mod tests {
|
|||
// use crate::prelude::*;
|
||||
use crate::event::{CreateEvent, ModifyEvent};
|
||||
use crate::idm::event::UnixPasswordChangeEvent;
|
||||
use crate::ldap::LdapServer;
|
||||
use crate::idm::serviceaccount::GenerateApiTokenEvent;
|
||||
use crate::ldap::{LdapServer, LdapSession};
|
||||
use async_std::task;
|
||||
use hashbrown::HashSet;
|
||||
use kanidm_proto::v1::ApiToken;
|
||||
use ldap3_proto::proto::{LdapFilter, LdapOp, LdapSearchScope};
|
||||
use ldap3_proto::simple::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
use compact_jwt::{Jws, JwsUnverified};
|
||||
|
||||
const TEST_PASSWORD: &'static str = "ntaoeuntnaoeuhraohuercahu😍";
|
||||
|
||||
|
@ -566,25 +595,26 @@ mod tests {
|
|||
let anon_t = task::block_on(ldaps.do_bind(idms, "", ""))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(anon_t.uuid == UUID_ANONYMOUS);
|
||||
assert!(task::block_on(ldaps.do_bind(idms, "", "test"))
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(anon_t.effective_session == LdapSession::UnixBind(UUID_ANONYMOUS));
|
||||
assert!(
|
||||
task::block_on(ldaps.do_bind(idms, "", "test")).unwrap_err()
|
||||
== OperationError::NotAuthenticated
|
||||
);
|
||||
|
||||
// Now test the admin and various DN's
|
||||
let admin_t = task::block_on(ldaps.do_bind(idms, "admin", TEST_PASSWORD))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t =
|
||||
task::block_on(ldaps.do_bind(idms, "admin@example.com", TEST_PASSWORD))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t = task::block_on(ldaps.do_bind(idms, STR_UUID_ADMIN, TEST_PASSWORD))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t = task::block_on(ldaps.do_bind(
|
||||
idms,
|
||||
"name=admin,dc=example,dc=com",
|
||||
|
@ -592,7 +622,7 @@ mod tests {
|
|||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t = task::block_on(ldaps.do_bind(
|
||||
idms,
|
||||
"spn=admin@example.com,dc=example,dc=com",
|
||||
|
@ -600,7 +630,7 @@ mod tests {
|
|||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t = task::block_on(ldaps.do_bind(
|
||||
idms,
|
||||
format!("uuid={},dc=example,dc=com", STR_UUID_ADMIN).as_str(),
|
||||
|
@ -608,17 +638,17 @@ mod tests {
|
|||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
|
||||
let admin_t = task::block_on(ldaps.do_bind(idms, "name=admin", TEST_PASSWORD))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t =
|
||||
task::block_on(ldaps.do_bind(idms, "spn=admin@example.com", TEST_PASSWORD))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t = task::block_on(ldaps.do_bind(
|
||||
idms,
|
||||
format!("uuid={}", STR_UUID_ADMIN).as_str(),
|
||||
|
@ -626,13 +656,13 @@ mod tests {
|
|||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
|
||||
let admin_t =
|
||||
task::block_on(ldaps.do_bind(idms, "admin,dc=example,dc=com", TEST_PASSWORD))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t = task::block_on(ldaps.do_bind(
|
||||
idms,
|
||||
"admin@example.com,dc=example,dc=com",
|
||||
|
@ -640,7 +670,7 @@ mod tests {
|
|||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
let admin_t = task::block_on(ldaps.do_bind(
|
||||
idms,
|
||||
format!("{},dc=example,dc=com", STR_UUID_ADMIN).as_str(),
|
||||
|
@ -648,7 +678,7 @@ mod tests {
|
|||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(admin_t.uuid == *UUID_ADMIN);
|
||||
assert!(admin_t.effective_session == LdapSession::UnixBind(*UUID_ADMIN));
|
||||
|
||||
// Bad password, check last to prevent softlocking of the admin account.
|
||||
assert!(task::block_on(ldaps.do_bind(idms, "admin", "test"))
|
||||
|
@ -745,7 +775,7 @@ mod tests {
|
|||
let anon_t = task::block_on(ldaps.do_bind(idms, "", ""))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(anon_t.uuid == UUID_ANONYMOUS);
|
||||
assert!(anon_t.effective_session == LdapSession::UnixBind(UUID_ANONYMOUS));
|
||||
|
||||
// Check that when we request *, we get default list.
|
||||
let sr = SearchRequest {
|
||||
|
@ -853,28 +883,140 @@ mod tests {
|
|||
fn test_ldap_token_privilege_granting() {
|
||||
run_idm_test!(
|
||||
|_qs: &QueryServer, idms: &IdmServer, _idms_delayed: &IdmServerDelayed| {
|
||||
// Setup the ldap server
|
||||
let ldaps = LdapServer::new(idms).expect("failed to start ldap");
|
||||
|
||||
// Prebuild the search req we'll be using this test.
|
||||
let sr = SearchRequest {
|
||||
msgid: 1,
|
||||
base: "dc=example,dc=com".to_string(),
|
||||
scope: LdapSearchScope::Subtree,
|
||||
filter: LdapFilter::Equality("name".to_string(), "testperson1".to_string()),
|
||||
attrs: vec!["name".to_string(), "mail".to_string()],
|
||||
};
|
||||
|
||||
let sa_uuid = uuid::uuid!("cc8e95b4-c24f-4d68-ba54-8bed76f63930");
|
||||
|
||||
// Configure the user account that will have the tokens issued.
|
||||
// Should be a SERVICE account.
|
||||
let apitoken = {
|
||||
// Create a service account,
|
||||
|
||||
// Should I finally do the class rules shit?
|
||||
let e1 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("service_account")),
|
||||
("class", Value::new_class("account")),
|
||||
("uuid", Value::new_uuid(sa_uuid)),
|
||||
("name", Value::new_iname("service_permission_test")),
|
||||
("displayname", Value::new_utf8s("service_permission_test"))
|
||||
);
|
||||
|
||||
// Issue a token, make it purpose = ldap.
|
||||
// Setup a person with an email
|
||||
let e2 = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("person")),
|
||||
("class", Value::new_class("account")),
|
||||
("class", Value::new_class("posixaccount")),
|
||||
("name", Value::new_iname("testperson1")),
|
||||
(
|
||||
"mail",
|
||||
Value::EmailAddress("testperson1@example.com".to_string(), true)
|
||||
),
|
||||
("description", Value::new_utf8s("testperson1")),
|
||||
("displayname", Value::new_utf8s("testperson1")),
|
||||
("gidnumber", Value::new_uint32(12345678)),
|
||||
("loginshell", Value::new_iutf8("/bin/zsh"))
|
||||
);
|
||||
|
||||
// assert the uat fails on non-ldap events.
|
||||
// Setup an access control for the service account to view mail attrs.
|
||||
|
||||
// Setup a person with
|
||||
// Setup an access control for the service account to view mail attrs.
|
||||
let ct = duration_from_epoch_now();
|
||||
|
||||
// Setup the ldap server
|
||||
let _ldaps = LdapServer::new(idms).expect("failed to start ldap");
|
||||
let server_txn = idms.proxy_write(ct);
|
||||
let ce = CreateEvent::new_internal(vec![e1, e2]);
|
||||
assert!(server_txn.qs_write.create(&ce).is_ok());
|
||||
|
||||
// Bind with account pw, search and show attr isn't accessible.
|
||||
// idm_people_read_priv
|
||||
let me = unsafe {
|
||||
ModifyEvent::new_internal_invalid(
|
||||
filter!(f_eq(
|
||||
"name",
|
||||
PartialValue::new_iname("idm_people_read_priv")
|
||||
)),
|
||||
ModifyList::new_list(vec![Modify::Present(
|
||||
AttrString::from("member"),
|
||||
Value::new_refer(sa_uuid),
|
||||
)]),
|
||||
)
|
||||
};
|
||||
assert!(server_txn.qs_write.modify(&me).is_ok());
|
||||
|
||||
// Bind using the token instead of the PW.
|
||||
// Issue a token
|
||||
// make it purpose = ldap <- currently purpose isn't supported,
|
||||
// it's an idea for future.
|
||||
let gte = GenerateApiTokenEvent::new_internal(sa_uuid, "TestToken", None);
|
||||
|
||||
// Search and retrieve an attribute that's now accessible.
|
||||
let apitoken = server_txn
|
||||
.service_account_generate_api_token(>e, ct)
|
||||
.expect("Failed to create new apitoken");
|
||||
|
||||
assert!(true);
|
||||
assert!(server_txn.commit().is_ok());
|
||||
|
||||
apitoken
|
||||
};
|
||||
|
||||
// assert the token fails on non-ldap events token-xchg <- currently
|
||||
// we don't have purpose so this isn't tested.
|
||||
|
||||
// Bind with anonymous, search and show mail attr isn't accessible.
|
||||
let anon_lbt = task::block_on(ldaps.do_bind(idms, "", ""))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(anon_lbt.effective_session == LdapSession::UnixBind(UUID_ANONYMOUS));
|
||||
|
||||
let r1 = task::block_on(ldaps.do_search(idms, &sr, &anon_lbt)).unwrap();
|
||||
assert!(r1.len() == 2);
|
||||
match &r1[0].op {
|
||||
LdapOp::SearchResultEntry(lsre) => {
|
||||
assert_entry_contains!(
|
||||
lsre,
|
||||
"spn=testperson1@example.com,dc=example,dc=com",
|
||||
("name", "testperson1")
|
||||
);
|
||||
}
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
// Inspect the token to get its uuid out.
|
||||
let apitoken_unverified =
|
||||
JwsUnverified::from_str(&apitoken).expect("Failed to parse apitoken");
|
||||
|
||||
let apitoken_inner: Jws<ApiToken> = apitoken_unverified
|
||||
.validate_embeded()
|
||||
.expect("Embedded jwk not found");
|
||||
|
||||
let apitoken_inner = apitoken_inner.into_inner();
|
||||
|
||||
// Bind using the token
|
||||
let sa_lbt = task::block_on(ldaps.do_bind(idms, "", &apitoken))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(sa_lbt.effective_session == LdapSession::ApiToken(apitoken_inner.clone()));
|
||||
|
||||
// Search and retrieve mail that's now accessible.
|
||||
let r1 = task::block_on(ldaps.do_search(idms, &sr, &sa_lbt)).unwrap();
|
||||
assert!(r1.len() == 2);
|
||||
match &r1[0].op {
|
||||
LdapOp::SearchResultEntry(lsre) => {
|
||||
assert_entry_contains!(
|
||||
lsre,
|
||||
"spn=testperson1@example.com,dc=example,dc=com",
|
||||
("name", "testperson1"),
|
||||
("mail", "testperson1@example.com")
|
||||
);
|
||||
}
|
||||
_ => assert!(false),
|
||||
};
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
//! The Kanidmd server library. This implements all of the internal components of the server
|
||||
//! which is used to process authentication, store identities and enforce access controls.
|
||||
|
||||
//#![deny(warnings)]
|
||||
|
||||
#![deny(warnings)]
|
||||
#![recursion_limit = "512"]
|
||||
#![warn(unused_extern_crates)]
|
||||
#![deny(clippy::todo)]
|
||||
|
|
|
@ -68,6 +68,11 @@ impl Plugin for GidNumber {
|
|||
"plugin_gidnumber"
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "debug",
|
||||
name = "gidnumber_pre_create_transform",
|
||||
skip(_qs, cand, _ce)
|
||||
)]
|
||||
fn pre_create_transform(
|
||||
_qs: &QueryServerWriteTransaction,
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
|
||||
|
@ -80,6 +85,7 @@ impl Plugin for GidNumber {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "gidnumber_pre_modify", skip(_qs, cand, _me))]
|
||||
fn pre_modify(
|
||||
_qs: &QueryServerWriteTransaction,
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
|
|
|
@ -7,11 +7,12 @@ use compact_jwt::JwsSigner;
|
|||
lazy_static! {
|
||||
static ref CLASS_OAUTH2_BASIC: PartialValue =
|
||||
PartialValue::new_class("oauth2_resource_server_basic");
|
||||
static ref CLASS_SERVICE_ACCOUNT: PartialValue = PartialValue::new_class("service_account");
|
||||
}
|
||||
|
||||
pub struct Oauth2Secrets {}
|
||||
pub struct JwsKeygen {}
|
||||
|
||||
macro_rules! oauth2_transform {
|
||||
macro_rules! keygen_transform {
|
||||
(
|
||||
$e:expr
|
||||
) => {{
|
||||
|
@ -52,18 +53,32 @@ macro_rules! oauth2_transform {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if $e.attribute_equality("class", &CLASS_SERVICE_ACCOUNT) {
|
||||
if !$e.attribute_pres("jws_es256_private_key") {
|
||||
security_info!("regenerating jws es256 private key");
|
||||
let jwssigner = JwsSigner::generate_es256()
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Unable to generate ES256 JwsSigner private key");
|
||||
OperationError::CryptographyError
|
||||
})?;
|
||||
let v = Value::JwsKeyEs256(jwssigner);
|
||||
$e.add_ava("jws_es256_private_key", v);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}};
|
||||
}
|
||||
|
||||
impl Plugin for Oauth2Secrets {
|
||||
impl Plugin for JwsKeygen {
|
||||
fn id() -> &'static str {
|
||||
"plugin_oauth2_secrets"
|
||||
"plugin_jws_keygen"
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "debug",
|
||||
name = "oauth2_pre_create_transform",
|
||||
name = "jwskeygen_pre_create_transform",
|
||||
skip(_qs, cand, _ce)
|
||||
)]
|
||||
fn pre_create_transform(
|
||||
|
@ -71,16 +86,16 @@ impl Plugin for Oauth2Secrets {
|
|||
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
|
||||
_ce: &CreateEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
cand.iter_mut().try_for_each(|e| oauth2_transform!(e))
|
||||
cand.iter_mut().try_for_each(|e| keygen_transform!(e))
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "oauth2_pre_modify", skip(_qs, cand, _me))]
|
||||
#[instrument(level = "debug", name = "jwskeygen_pre_modify", skip(_qs, cand, _me))]
|
||||
fn pre_modify(
|
||||
_qs: &QueryServerWriteTransaction,
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
_me: &ModifyEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
cand.iter_mut().try_for_each(|e| oauth2_transform!(e))
|
||||
cand.iter_mut().try_for_each(|e| keygen_transform!(e))
|
||||
}
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@ mod domain;
|
|||
pub(crate) mod dyngroup;
|
||||
mod failure;
|
||||
mod gidnumber;
|
||||
mod jwskeygen;
|
||||
mod memberof;
|
||||
mod oauth2;
|
||||
mod password_import;
|
||||
mod protected;
|
||||
mod recycle;
|
||||
|
@ -126,7 +126,7 @@ impl Plugins {
|
|||
spanned!("plugins::run_pre_create_transform", {
|
||||
base::Base::pre_create_transform(qs, cand, ce)
|
||||
.and_then(|_| password_import::PasswordImport::pre_create_transform(qs, cand, ce))
|
||||
.and_then(|_| oauth2::Oauth2Secrets::pre_create_transform(qs, cand, ce))
|
||||
.and_then(|_| jwskeygen::JwsKeygen::pre_create_transform(qs, cand, ce))
|
||||
.and_then(|_| gidnumber::GidNumber::pre_create_transform(qs, cand, ce))
|
||||
.and_then(|_| domain::Domain::pre_create_transform(qs, cand, ce))
|
||||
.and_then(|_| spn::Spn::pre_create_transform(qs, cand, ce))
|
||||
|
@ -165,7 +165,7 @@ impl Plugins {
|
|||
protected::Protected::pre_modify(qs, cand, me)
|
||||
.and_then(|_| base::Base::pre_modify(qs, cand, me))
|
||||
.and_then(|_| password_import::PasswordImport::pre_modify(qs, cand, me))
|
||||
.and_then(|_| oauth2::Oauth2Secrets::pre_modify(qs, cand, me))
|
||||
.and_then(|_| jwskeygen::JwsKeygen::pre_modify(qs, cand, me))
|
||||
.and_then(|_| gidnumber::GidNumber::pre_modify(qs, cand, me))
|
||||
.and_then(|_| domain::Domain::pre_modify(qs, cand, me))
|
||||
.and_then(|_| spn::Spn::pre_modify(qs, cand, me))
|
||||
|
|
|
@ -172,31 +172,36 @@ impl SchemaAttribute {
|
|||
// on that may only use a single tagged attribute for example.
|
||||
pub fn validate_partialvalue(&self, a: &str, v: &PartialValue) -> Result<(), SchemaError> {
|
||||
let r = match self.syntax {
|
||||
SyntaxType::Boolean => v.is_bool(),
|
||||
SyntaxType::SYNTAX_ID => v.is_syntax(),
|
||||
SyntaxType::INDEX_ID => v.is_index(),
|
||||
SyntaxType::Uuid => v.is_uuid(),
|
||||
SyntaxType::REFERENCE_UUID => v.is_refer(),
|
||||
SyntaxType::Utf8StringInsensitive => v.is_iutf8(),
|
||||
SyntaxType::Utf8StringIname => v.is_iname(),
|
||||
SyntaxType::UTF8STRING => v.is_utf8(),
|
||||
SyntaxType::JSON_FILTER => v.is_json_filter(),
|
||||
SyntaxType::Credential => v.is_credential(),
|
||||
SyntaxType::SecretUtf8String => v.is_secret_string(),
|
||||
SyntaxType::SshKey => v.is_sshkey(),
|
||||
SyntaxType::SecurityPrincipalName => v.is_spn(),
|
||||
SyntaxType::UINT32 => v.is_uint32(),
|
||||
SyntaxType::Cid => v.is_cid(),
|
||||
SyntaxType::NsUniqueId => v.is_nsuniqueid(),
|
||||
SyntaxType::DateTime => v.is_datetime(),
|
||||
SyntaxType::EmailAddress => v.is_email_address(),
|
||||
SyntaxType::Url => v.is_url(),
|
||||
SyntaxType::OauthScope => v.is_oauthscope(),
|
||||
SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(),
|
||||
SyntaxType::PrivateBinary => v.is_privatebinary(),
|
||||
SyntaxType::Boolean => matches!(v, PartialValue::Bool(_)),
|
||||
SyntaxType::SyntaxId => matches!(v, PartialValue::Syntax(_)),
|
||||
SyntaxType::IndexId => matches!(v, PartialValue::Index(_)),
|
||||
SyntaxType::Uuid => matches!(v, PartialValue::Uuid(_)),
|
||||
SyntaxType::ReferenceUuid => matches!(v, PartialValue::Refer(_)),
|
||||
SyntaxType::Utf8StringInsensitive => matches!(v, PartialValue::Iutf8(_)),
|
||||
SyntaxType::Utf8StringIname => matches!(v, PartialValue::Iname(_)),
|
||||
SyntaxType::Utf8String => matches!(v, PartialValue::Utf8(_)),
|
||||
SyntaxType::JsonFilter => matches!(v, PartialValue::JsonFilt(_)),
|
||||
SyntaxType::Credential => matches!(v, PartialValue::Cred(_)),
|
||||
SyntaxType::SecretUtf8String => matches!(v, PartialValue::SecretValue),
|
||||
SyntaxType::SshKey => matches!(v, PartialValue::SshKey(_)),
|
||||
SyntaxType::SecurityPrincipalName => matches!(v, PartialValue::Spn(_, _)),
|
||||
SyntaxType::Uint32 => matches!(v, PartialValue::Uint32(_)),
|
||||
SyntaxType::Cid => matches!(v, PartialValue::Cid(_)),
|
||||
SyntaxType::NsUniqueId => matches!(v, PartialValue::Nsuniqueid(_)),
|
||||
SyntaxType::DateTime => matches!(v, PartialValue::DateTime(_)),
|
||||
SyntaxType::EmailAddress => matches!(v, PartialValue::EmailAddress(_)),
|
||||
SyntaxType::Url => matches!(v, PartialValue::Url(_)),
|
||||
SyntaxType::OauthScope => matches!(v, PartialValue::OauthScope(_)),
|
||||
SyntaxType::OauthScopeMap => matches!(v, PartialValue::Refer(_)),
|
||||
SyntaxType::PrivateBinary => matches!(v, PartialValue::PrivateBinary),
|
||||
SyntaxType::IntentToken => matches!(v, PartialValue::IntentToken(_)),
|
||||
SyntaxType::Passkey => matches!(v, PartialValue::Passkey(_)),
|
||||
SyntaxType::DeviceKey => matches!(v, PartialValue::DeviceKey(_)),
|
||||
// Allow refer types.
|
||||
SyntaxType::Session => matches!(v, PartialValue::Refer(_)),
|
||||
// These are just insensitive string lookups on the hex-ified kid.
|
||||
SyntaxType::JwsKeyEs256 => matches!(v, PartialValue::Iutf8(_)),
|
||||
SyntaxType::JwsKeyRs256 => matches!(v, PartialValue::Iutf8(_)),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
|
@ -214,31 +219,34 @@ impl SchemaAttribute {
|
|||
pub fn validate_value(&self, a: &str, v: &Value) -> Result<(), SchemaError> {
|
||||
let r = v.validate()
|
||||
&& match self.syntax {
|
||||
SyntaxType::Boolean => v.is_bool(),
|
||||
SyntaxType::SYNTAX_ID => v.is_syntax(),
|
||||
SyntaxType::INDEX_ID => v.is_index(),
|
||||
SyntaxType::Uuid => v.is_uuid(),
|
||||
SyntaxType::REFERENCE_UUID => v.is_refer(),
|
||||
SyntaxType::Utf8StringInsensitive => v.is_iutf8(),
|
||||
SyntaxType::Utf8StringIname => v.is_iname(),
|
||||
SyntaxType::UTF8STRING => v.is_utf8(),
|
||||
SyntaxType::JSON_FILTER => v.is_json_filter(),
|
||||
SyntaxType::Credential => v.is_credential(),
|
||||
SyntaxType::SecretUtf8String => v.is_secret_string(),
|
||||
SyntaxType::SshKey => v.is_sshkey(),
|
||||
SyntaxType::SecurityPrincipalName => v.is_spn(),
|
||||
SyntaxType::UINT32 => v.is_uint32(),
|
||||
SyntaxType::Cid => v.is_cid(),
|
||||
SyntaxType::NsUniqueId => v.is_nsuniqueid(),
|
||||
SyntaxType::DateTime => v.is_datetime(),
|
||||
SyntaxType::EmailAddress => v.is_email_address(),
|
||||
SyntaxType::Url => v.is_url(),
|
||||
SyntaxType::OauthScope => v.is_oauthscope(),
|
||||
SyntaxType::OauthScopeMap => v.is_oauthscopemap() || v.is_refer(),
|
||||
SyntaxType::PrivateBinary => v.is_privatebinary(),
|
||||
SyntaxType::Boolean => matches!(v, Value::Bool(_)),
|
||||
SyntaxType::SyntaxId => matches!(v, Value::Syntax(_)),
|
||||
SyntaxType::IndexId => matches!(v, Value::Index(_)),
|
||||
SyntaxType::Uuid => matches!(v, Value::Uuid(_)),
|
||||
SyntaxType::ReferenceUuid => matches!(v, Value::Refer(_)),
|
||||
SyntaxType::Utf8StringInsensitive => matches!(v, Value::Iutf8(_)),
|
||||
SyntaxType::Utf8StringIname => matches!(v, Value::Iname(_)),
|
||||
SyntaxType::Utf8String => matches!(v, Value::Utf8(_)),
|
||||
SyntaxType::JsonFilter => matches!(v, Value::JsonFilt(_)),
|
||||
SyntaxType::Credential => matches!(v, Value::Cred(_, _)),
|
||||
SyntaxType::SecretUtf8String => matches!(v, Value::SecretValue(_)),
|
||||
SyntaxType::SshKey => matches!(v, Value::SshKey(_, _)),
|
||||
SyntaxType::SecurityPrincipalName => matches!(v, Value::Spn(_, _)),
|
||||
SyntaxType::Uint32 => matches!(v, Value::Uint32(_)),
|
||||
SyntaxType::Cid => matches!(v, Value::Cid(_)),
|
||||
SyntaxType::NsUniqueId => matches!(v, Value::Nsuniqueid(_)),
|
||||
SyntaxType::DateTime => matches!(v, Value::DateTime(_)),
|
||||
SyntaxType::EmailAddress => matches!(v, Value::EmailAddress(_, _)),
|
||||
SyntaxType::Url => matches!(v, Value::Url(_)),
|
||||
SyntaxType::OauthScope => matches!(v, Value::OauthScope(_)),
|
||||
SyntaxType::OauthScopeMap => matches!(v, Value::OauthScopeMap(_, _)),
|
||||
SyntaxType::PrivateBinary => matches!(v, Value::PrivateBinary(_)),
|
||||
SyntaxType::IntentToken => matches!(v, Value::IntentToken(_, _)),
|
||||
SyntaxType::Passkey => matches!(v, Value::Passkey(_, _, _)),
|
||||
SyntaxType::DeviceKey => matches!(v, Value::DeviceKey(_, _, _)),
|
||||
SyntaxType::Session => matches!(v, Value::Session(_, _)),
|
||||
SyntaxType::JwsKeyEs256 => matches!(v, Value::JwsKeyEs256(_)),
|
||||
SyntaxType::JwsKeyRs256 => matches!(v, Value::JwsKeyRs256(_)),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
|
@ -580,7 +588,10 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
// No, they'll over-write each other ... but we do need name uniqueness.
|
||||
attributetypes.into_iter().for_each(|a| {
|
||||
// Update the unique and ref caches.
|
||||
if a.syntax == SyntaxType::REFERENCE_UUID || a.syntax == SyntaxType::OauthScopeMap {
|
||||
if a.syntax == SyntaxType::ReferenceUuid || a.syntax == SyntaxType::OauthScopeMap
|
||||
// May not need to be a ref type since it doesn't have external links/impact?
|
||||
// || a.syntax == SyntaxType::Session
|
||||
{
|
||||
self.ref_cache.insert(a.name.clone(), a.clone());
|
||||
}
|
||||
if a.unique {
|
||||
|
@ -745,7 +756,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![],
|
||||
syntax: SyntaxType::UTF8STRING,
|
||||
syntax: SyntaxType::Utf8String,
|
||||
},
|
||||
);
|
||||
self.attributes.insert(AttrString::from("multivalue"), SchemaAttribute {
|
||||
|
@ -790,7 +801,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![],
|
||||
syntax: SyntaxType::INDEX_ID,
|
||||
syntax: SyntaxType::IndexId,
|
||||
},
|
||||
);
|
||||
self.attributes.insert(
|
||||
|
@ -805,7 +816,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality],
|
||||
syntax: SyntaxType::SYNTAX_ID,
|
||||
syntax: SyntaxType::SyntaxId,
|
||||
},
|
||||
);
|
||||
self.attributes.insert(
|
||||
|
@ -957,7 +968,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality, IndexType::SubString],
|
||||
syntax: SyntaxType::JSON_FILTER,
|
||||
syntax: SyntaxType::JsonFilter,
|
||||
},
|
||||
);
|
||||
self.attributes.insert(
|
||||
|
@ -972,7 +983,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality, IndexType::SubString],
|
||||
syntax: SyntaxType::JSON_FILTER,
|
||||
syntax: SyntaxType::JsonFilter,
|
||||
},
|
||||
);
|
||||
self.attributes.insert(
|
||||
|
@ -1069,7 +1080,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality],
|
||||
syntax: SyntaxType::REFERENCE_UUID,
|
||||
syntax: SyntaxType::ReferenceUuid,
|
||||
},
|
||||
);
|
||||
self.attributes.insert(
|
||||
|
@ -1082,7 +1093,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality],
|
||||
syntax: SyntaxType::REFERENCE_UUID,
|
||||
syntax: SyntaxType::ReferenceUuid,
|
||||
},
|
||||
);
|
||||
self.attributes.insert(
|
||||
|
@ -1095,7 +1106,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality],
|
||||
syntax: SyntaxType::REFERENCE_UUID,
|
||||
syntax: SyntaxType::ReferenceUuid,
|
||||
},
|
||||
);
|
||||
// Migration related
|
||||
|
@ -1111,7 +1122,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![],
|
||||
syntax: SyntaxType::UINT32,
|
||||
syntax: SyntaxType::Uint32,
|
||||
},
|
||||
);
|
||||
// Domain for sysinfo
|
||||
|
@ -1169,7 +1180,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: true,
|
||||
index: vec![],
|
||||
syntax: SyntaxType::UTF8STRING,
|
||||
syntax: SyntaxType::Utf8String,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -1301,7 +1312,7 @@ impl<'a> SchemaWriteTransaction<'a> {
|
|||
unique: false,
|
||||
phantom: true,
|
||||
index: vec![],
|
||||
syntax: SyntaxType::UINT32,
|
||||
syntax: SyntaxType::Uint32,
|
||||
},
|
||||
);
|
||||
// end LDAP masking phantoms
|
||||
|
@ -1906,7 +1917,7 @@ mod tests {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality],
|
||||
syntax: SyntaxType::UTF8STRING,
|
||||
syntax: SyntaxType::Utf8String,
|
||||
};
|
||||
|
||||
let rvs = vs_utf8!["test1".to_string(), "test2".to_string()] as _;
|
||||
|
@ -1955,7 +1966,7 @@ mod tests {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality],
|
||||
syntax: SyntaxType::SYNTAX_ID,
|
||||
syntax: SyntaxType::SyntaxId,
|
||||
};
|
||||
|
||||
let rvs = vs_syntax![SyntaxType::try_from("UTF8STRING").unwrap()] as _;
|
||||
|
@ -1978,7 +1989,7 @@ mod tests {
|
|||
unique: false,
|
||||
phantom: false,
|
||||
index: vec![IndexType::Equality],
|
||||
syntax: SyntaxType::INDEX_ID,
|
||||
syntax: SyntaxType::IndexId,
|
||||
};
|
||||
//
|
||||
let rvs = vs_index![IndexType::try_from("EQUALITY").unwrap()] as _;
|
||||
|
|
|
@ -53,6 +53,7 @@ lazy_static! {
|
|||
static ref PVCLASS_OAUTH2_RS: PartialValue = PartialValue::new_class("oauth2_resource_server");
|
||||
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
|
||||
static ref PVCLASS_PERSON: PartialValue = PartialValue::new_class("person");
|
||||
static ref PVCLASS_SERVICE_ACCOUNT: PartialValue = PartialValue::new_class("service_account");
|
||||
static ref PVUUID_DOMAIN_INFO: PartialValue = PartialValue::new_uuid(*UUID_DOMAIN_INFO);
|
||||
static ref PVACP_ENABLE_FALSE: PartialValue = PartialValue::new_bool(false);
|
||||
}
|
||||
|
@ -94,8 +95,9 @@ pub struct QueryServerReadTransaction<'a> {
|
|||
schema: SchemaReadTransaction,
|
||||
accesscontrols: AccessControlsReadTransaction<'a>,
|
||||
_db_ticket: SemaphorePermit<'a>,
|
||||
resolve_filter_cache:
|
||||
Cell<ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
|
||||
resolve_filter_cache: Cell<
|
||||
ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>,
|
||||
>,
|
||||
}
|
||||
|
||||
unsafe impl<'a> Sync for QueryServerReadTransaction<'a> {}
|
||||
|
@ -121,8 +123,9 @@ pub struct QueryServerWriteTransaction<'a> {
|
|||
changed_uuid: Cell<HashSet<Uuid>>,
|
||||
_db_ticket: SemaphorePermit<'a>,
|
||||
_write_ticket: SemaphorePermit<'a>,
|
||||
resolve_filter_cache:
|
||||
Cell<ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
|
||||
resolve_filter_cache: Cell<
|
||||
ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>,
|
||||
>,
|
||||
dyngroup_cache: Cell<CowCellWriteTxn<'a, DynGroupCache>>,
|
||||
}
|
||||
|
||||
|
@ -163,7 +166,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
#[allow(clippy::mut_from_ref)]
|
||||
fn get_resolve_filter_cache(
|
||||
&self,
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>;
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>;
|
||||
|
||||
/// Conduct a search and apply access controls to yield a set of entries that
|
||||
/// have been reduced to the set of user visible avas. Note that if you provide
|
||||
|
@ -475,14 +478,14 @@ pub trait QueryServerTransaction<'a> {
|
|||
match schema.get_attributes().get(attr) {
|
||||
Some(schema_a) => {
|
||||
match schema_a.syntax {
|
||||
SyntaxType::UTF8STRING => Ok(Value::new_utf8(value.to_string())),
|
||||
SyntaxType::Utf8String => Ok(Value::new_utf8(value.to_string())),
|
||||
SyntaxType::Utf8StringInsensitive => Ok(Value::new_iutf8(value)),
|
||||
SyntaxType::Utf8StringIname => Ok(Value::new_iname(value)),
|
||||
SyntaxType::Boolean => Value::new_bools(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid boolean syntax".to_string())),
|
||||
SyntaxType::SYNTAX_ID => Value::new_syntaxs(value)
|
||||
SyntaxType::SyntaxId => Value::new_syntaxs(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Syntax syntax".to_string())),
|
||||
SyntaxType::INDEX_ID => Value::new_indexs(value)
|
||||
SyntaxType::IndexId => Value::new_indexs(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Index syntax".to_string())),
|
||||
SyntaxType::Uuid => {
|
||||
// It's a uuid - we do NOT check for existance, because that
|
||||
|
@ -503,7 +506,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
// I think this is unreachable due to how the .or_else works.
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid UUID syntax".to_string()))
|
||||
}
|
||||
SyntaxType::REFERENCE_UUID => {
|
||||
SyntaxType::ReferenceUuid => {
|
||||
// See comments above.
|
||||
Value::new_refer_s(value)
|
||||
.or_else(|| {
|
||||
|
@ -515,13 +518,13 @@ pub trait QueryServerTransaction<'a> {
|
|||
// I think this is unreachable due to how the .or_else works.
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Reference syntax".to_string()))
|
||||
}
|
||||
SyntaxType::JSON_FILTER => Value::new_json_filter_s(value)
|
||||
SyntaxType::JsonFilter => Value::new_json_filter_s(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Filter syntax".to_string())),
|
||||
SyntaxType::Credential => Err(OperationError::InvalidAttribute("Credentials can not be supplied through modification - please use the IDM api".to_string())),
|
||||
SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute("Radius secrets can not be supplied through modification - please use the IDM api".to_string())),
|
||||
SyntaxType::SshKey => Err(OperationError::InvalidAttribute("SSH public keys can not be supplied through modification - please use the IDM api".to_string())),
|
||||
SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute("SPNs are generated and not able to be set.".to_string())),
|
||||
SyntaxType::UINT32 => Value::new_uint32_str(value)
|
||||
SyntaxType::Uint32 => Value::new_uint32_str(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid uint32 syntax".to_string())),
|
||||
SyntaxType::Cid => Err(OperationError::InvalidAttribute("CIDs are generated and not able to be set.".to_string())),
|
||||
SyntaxType::NsUniqueId => Value::new_nsuniqueid_s(value)
|
||||
|
@ -539,6 +542,9 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::IntentToken => Err(OperationError::InvalidAttribute("Intent Token Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::Passkey => Err(OperationError::InvalidAttribute("Passkey Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::DeviceKey => Err(OperationError::InvalidAttribute("DeviceKey Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::Session => Err(OperationError::InvalidAttribute("Session Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::JwsKeyEs256 => Err(OperationError::InvalidAttribute("JwsKeyEs256 Values can not be supplied through modification".to_string())),
|
||||
SyntaxType::JwsKeyRs256 => Err(OperationError::InvalidAttribute("JwsKeyRs256 Values can not be supplied through modification".to_string())),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -556,16 +562,18 @@ pub trait QueryServerTransaction<'a> {
|
|||
match schema.get_attributes().get(attr) {
|
||||
Some(schema_a) => {
|
||||
match schema_a.syntax {
|
||||
SyntaxType::UTF8STRING => Ok(PartialValue::new_utf8(value.to_string())),
|
||||
SyntaxType::Utf8StringInsensitive => Ok(PartialValue::new_iutf8(value)),
|
||||
SyntaxType::Utf8String => Ok(PartialValue::new_utf8(value.to_string())),
|
||||
SyntaxType::Utf8StringInsensitive
|
||||
| SyntaxType::JwsKeyEs256
|
||||
| SyntaxType::JwsKeyRs256 => Ok(PartialValue::new_iutf8(value)),
|
||||
SyntaxType::Utf8StringIname => Ok(PartialValue::new_iname(value)),
|
||||
SyntaxType::Boolean => PartialValue::new_bools(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute("Invalid boolean syntax".to_string())
|
||||
}),
|
||||
SyntaxType::SYNTAX_ID => PartialValue::new_syntaxs(value).ok_or_else(|| {
|
||||
SyntaxType::SyntaxId => PartialValue::new_syntaxs(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute("Invalid Syntax syntax".to_string())
|
||||
}),
|
||||
SyntaxType::INDEX_ID => PartialValue::new_indexs(value).ok_or_else(|| {
|
||||
SyntaxType::IndexId => PartialValue::new_indexs(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute("Invalid Index syntax".to_string())
|
||||
}),
|
||||
SyntaxType::Uuid => {
|
||||
|
@ -595,7 +603,10 @@ pub trait QueryServerTransaction<'a> {
|
|||
// PartialValue::new_uuid(un)
|
||||
// }))
|
||||
}
|
||||
SyntaxType::REFERENCE_UUID => {
|
||||
// ⚠️ Any types here need to also be added to update_attributes in
|
||||
// schema.rs for reference type / cache awareness during referential
|
||||
// integrity processing. Exceptions are self-contained value types!
|
||||
SyntaxType::ReferenceUuid | SyntaxType::OauthScopeMap | SyntaxType::Session => {
|
||||
// See comments above.
|
||||
PartialValue::new_refer_s(value)
|
||||
.or_else(|| {
|
||||
|
@ -610,23 +621,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
)
|
||||
})
|
||||
}
|
||||
SyntaxType::OauthScopeMap => {
|
||||
// See comments above.
|
||||
PartialValue::new_oauthscopemap_s(value)
|
||||
.or_else(|| {
|
||||
let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
|
||||
Some(PartialValue::new_oauthscopemap(un))
|
||||
})
|
||||
// I think this is unreachable due to how the .or_else works.
|
||||
// See above case for how to avoid having unreachable code
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAttribute(
|
||||
"Invalid Reference syntax".to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
SyntaxType::JSON_FILTER => {
|
||||
SyntaxType::JsonFilter => {
|
||||
PartialValue::new_json_filter_s(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute("Invalid Filter syntax".to_string())
|
||||
})
|
||||
|
@ -639,7 +634,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
OperationError::InvalidAttribute("Invalid spn syntax".to_string())
|
||||
})
|
||||
}
|
||||
SyntaxType::UINT32 => PartialValue::new_uint32_str(value).ok_or_else(|| {
|
||||
SyntaxType::Uint32 => PartialValue::new_uint32_str(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute("Invalid uint32 syntax".to_string())
|
||||
}),
|
||||
SyntaxType::Cid => PartialValue::new_cid_s(value).ok_or_else(|| {
|
||||
|
@ -832,7 +827,7 @@ impl<'a> QueryServerTransaction<'a> for QueryServerReadTransaction<'a> {
|
|||
|
||||
fn get_resolve_filter_cache(
|
||||
&self,
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
|
||||
{
|
||||
unsafe {
|
||||
let mptr = self.resolve_filter_cache.as_ptr();
|
||||
|
@ -841,6 +836,7 @@ impl<'a> QueryServerTransaction<'a> for QueryServerReadTransaction<'a> {
|
|||
'a,
|
||||
(IdentityId, Filter<FilterValid>),
|
||||
Filter<FilterValidResolved>,
|
||||
(),
|
||||
>
|
||||
}
|
||||
}
|
||||
|
@ -942,7 +938,7 @@ impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
|
|||
|
||||
fn get_resolve_filter_cache(
|
||||
&self,
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>
|
||||
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
|
||||
{
|
||||
unsafe {
|
||||
let mptr = self.resolve_filter_cache.as_ptr();
|
||||
|
@ -951,6 +947,7 @@ impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
|
|||
'a,
|
||||
(IdentityId, Filter<FilterValid>),
|
||||
Filter<FilterValidResolved>,
|
||||
(),
|
||||
>
|
||||
}
|
||||
}
|
||||
|
@ -1200,6 +1197,10 @@ impl QueryServer {
|
|||
migrate_txn.migrate_6_to_7()?;
|
||||
}
|
||||
|
||||
if system_info_version < 8 {
|
||||
migrate_txn.migrate_7_to_8()?;
|
||||
}
|
||||
|
||||
migrate_txn.commit()?;
|
||||
// Migrations complete. Init idm will now set the version as needed.
|
||||
|
||||
|
@ -2319,7 +2320,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
/// Modify accounts that are not persons, to be service accounts so that the extension
|
||||
/// rules remain valid.
|
||||
pub fn migrate_6_to_7(&self) -> Result<(), OperationError> {
|
||||
spanned!("server::migrate_5_to_6", {
|
||||
spanned!("server::migrate_6_to_7", {
|
||||
admin_warn!("starting 6 to 7 migration.");
|
||||
let filter = filter!(f_and!([
|
||||
f_eq("class", (*PVCLASS_ACCOUNT).clone()),
|
||||
|
@ -2331,6 +2332,19 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Migrate 7 to 8
|
||||
///
|
||||
/// Touch all service accounts to trigger a regen of their es256 jws keys for api tokens
|
||||
pub fn migrate_7_to_8(&self) -> Result<(), OperationError> {
|
||||
spanned!("server::migrate_7_to_8", {
|
||||
admin_warn!("starting 7 to 8 migration.");
|
||||
let filter = filter!(f_eq("class", (*PVCLASS_SERVICE_ACCOUNT).clone()));
|
||||
let modlist = ModifyList::new_append("class", Value::new_class("service_account"));
|
||||
self.internal_modify(&filter, &modlist)
|
||||
// Complete
|
||||
})
|
||||
}
|
||||
|
||||
// These are where searches and other actions are actually implemented. This
|
||||
// is the "internal" version, where we define the event as being internal
|
||||
// only, allowing certain plugin by passes etc.
|
||||
|
@ -2633,6 +2647,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_PASSKEYS,
|
||||
JSON_SCHEMA_ATTR_DEVICEKEYS,
|
||||
JSON_SCHEMA_ATTR_DYNGROUP_FILTER,
|
||||
JSON_SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY,
|
||||
JSON_SCHEMA_ATTR_API_TOKEN_SESSION,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_ORGPERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
|
||||
use crate::be::dbentry::DbIdentSpn;
|
||||
use crate::credential::Credential;
|
||||
use crate::identity::IdentityId;
|
||||
use crate::repl::cid::Cid;
|
||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||
|
||||
use compact_jwt::JwsSigner;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
use std::convert::TryFrom;
|
||||
|
@ -149,32 +151,36 @@ impl fmt::Display for IndexType {
|
|||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
|
||||
#[repr(u16)]
|
||||
pub enum SyntaxType {
|
||||
UTF8STRING,
|
||||
Utf8StringInsensitive,
|
||||
Utf8StringIname,
|
||||
Uuid,
|
||||
Boolean,
|
||||
SYNTAX_ID,
|
||||
INDEX_ID,
|
||||
REFERENCE_UUID,
|
||||
JSON_FILTER,
|
||||
Credential,
|
||||
SecretUtf8String,
|
||||
SshKey,
|
||||
SecurityPrincipalName,
|
||||
UINT32,
|
||||
Cid,
|
||||
NsUniqueId,
|
||||
DateTime,
|
||||
EmailAddress,
|
||||
Url,
|
||||
OauthScope,
|
||||
OauthScopeMap,
|
||||
PrivateBinary,
|
||||
IntentToken,
|
||||
Passkey,
|
||||
DeviceKey,
|
||||
Utf8String = 0,
|
||||
Utf8StringInsensitive = 1,
|
||||
Uuid = 2,
|
||||
Boolean = 3,
|
||||
SyntaxId = 4,
|
||||
IndexId = 5,
|
||||
ReferenceUuid = 6,
|
||||
JsonFilter = 7,
|
||||
Credential = 8,
|
||||
SecretUtf8String = 9,
|
||||
SshKey = 10,
|
||||
SecurityPrincipalName = 11,
|
||||
Uint32 = 12,
|
||||
Cid = 13,
|
||||
Utf8StringIname = 14,
|
||||
NsUniqueId = 15,
|
||||
DateTime = 16,
|
||||
EmailAddress = 17,
|
||||
Url = 18,
|
||||
OauthScope = 19,
|
||||
OauthScopeMap = 20,
|
||||
PrivateBinary = 21,
|
||||
IntentToken = 22,
|
||||
Passkey = 23,
|
||||
DeviceKey = 24,
|
||||
Session = 25,
|
||||
JwsKeyEs256 = 26,
|
||||
JwsKeyRs256 = 27,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -183,21 +189,21 @@ impl TryFrom<&str> for SyntaxType {
|
|||
fn try_from(value: &str) -> Result<SyntaxType, Self::Error> {
|
||||
let n_value = value.to_uppercase();
|
||||
match n_value.as_str() {
|
||||
"UTF8STRING" => Ok(SyntaxType::UTF8STRING),
|
||||
"UTF8STRING" => Ok(SyntaxType::Utf8String),
|
||||
"UTF8STRING_INSENSITIVE" => Ok(SyntaxType::Utf8StringInsensitive),
|
||||
"UTF8STRING_INAME" => Ok(SyntaxType::Utf8StringIname),
|
||||
"UUID" => Ok(SyntaxType::Uuid),
|
||||
"BOOLEAN" => Ok(SyntaxType::Boolean),
|
||||
"SYNTAX_ID" => Ok(SyntaxType::SYNTAX_ID),
|
||||
"INDEX_ID" => Ok(SyntaxType::INDEX_ID),
|
||||
"REFERENCE_UUID" => Ok(SyntaxType::REFERENCE_UUID),
|
||||
"JSON_FILTER" => Ok(SyntaxType::JSON_FILTER),
|
||||
"SYNTAX_ID" => Ok(SyntaxType::SyntaxId),
|
||||
"INDEX_ID" => Ok(SyntaxType::IndexId),
|
||||
"REFERENCE_UUID" => Ok(SyntaxType::ReferenceUuid),
|
||||
"JSON_FILTER" => Ok(SyntaxType::JsonFilter),
|
||||
"CREDENTIAL" => Ok(SyntaxType::Credential),
|
||||
// Compatability for older syntax name.
|
||||
"RADIUS_UTF8STRING" | "SECRET_UTF8STRING" => Ok(SyntaxType::SecretUtf8String),
|
||||
"SSHKEY" => Ok(SyntaxType::SshKey),
|
||||
"SECURITY_PRINCIPAL_NAME" => Ok(SyntaxType::SecurityPrincipalName),
|
||||
"UINT32" => Ok(SyntaxType::UINT32),
|
||||
"UINT32" => Ok(SyntaxType::Uint32),
|
||||
"CID" => Ok(SyntaxType::Cid),
|
||||
"NSUNIQUEID" => Ok(SyntaxType::NsUniqueId),
|
||||
"DATETIME" => Ok(SyntaxType::DateTime),
|
||||
|
@ -209,29 +215,32 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"INTENT_TOKEN" => Ok(SyntaxType::IntentToken),
|
||||
"PASSKEY" => Ok(SyntaxType::Passkey),
|
||||
"DEVICEKEY" => Ok(SyntaxType::DeviceKey),
|
||||
"SESSION" => Ok(SyntaxType::Session),
|
||||
"JWS_KEY_ES256" => Ok(SyntaxType::JwsKeyEs256),
|
||||
"JWS_KEY_RS256" => Ok(SyntaxType::JwsKeyRs256),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<usize> for SyntaxType {
|
||||
impl TryFrom<u16> for SyntaxType {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(SyntaxType::UTF8STRING),
|
||||
0 => Ok(SyntaxType::Utf8String),
|
||||
1 => Ok(SyntaxType::Utf8StringInsensitive),
|
||||
2 => Ok(SyntaxType::Uuid),
|
||||
3 => Ok(SyntaxType::Boolean),
|
||||
4 => Ok(SyntaxType::SYNTAX_ID),
|
||||
5 => Ok(SyntaxType::INDEX_ID),
|
||||
6 => Ok(SyntaxType::REFERENCE_UUID),
|
||||
7 => Ok(SyntaxType::JSON_FILTER),
|
||||
4 => Ok(SyntaxType::SyntaxId),
|
||||
5 => Ok(SyntaxType::IndexId),
|
||||
6 => Ok(SyntaxType::ReferenceUuid),
|
||||
7 => Ok(SyntaxType::JsonFilter),
|
||||
8 => Ok(SyntaxType::Credential),
|
||||
9 => Ok(SyntaxType::SecretUtf8String),
|
||||
10 => Ok(SyntaxType::SshKey),
|
||||
11 => Ok(SyntaxType::SecurityPrincipalName),
|
||||
12 => Ok(SyntaxType::UINT32),
|
||||
12 => Ok(SyntaxType::Uint32),
|
||||
13 => Ok(SyntaxType::Cid),
|
||||
14 => Ok(SyntaxType::Utf8StringIname),
|
||||
15 => Ok(SyntaxType::NsUniqueId),
|
||||
|
@ -244,60 +253,31 @@ impl TryFrom<usize> for SyntaxType {
|
|||
22 => Ok(SyntaxType::IntentToken),
|
||||
23 => Ok(SyntaxType::Passkey),
|
||||
24 => Ok(SyntaxType::DeviceKey),
|
||||
25 => Ok(SyntaxType::Session),
|
||||
26 => Ok(SyntaxType::JwsKeyEs256),
|
||||
27 => Ok(SyntaxType::JwsKeyRs256),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SyntaxType {
|
||||
pub fn to_usize(&self) -> usize {
|
||||
match self {
|
||||
SyntaxType::UTF8STRING => 0,
|
||||
SyntaxType::Utf8StringInsensitive => 1,
|
||||
SyntaxType::Uuid => 2,
|
||||
SyntaxType::Boolean => 3,
|
||||
SyntaxType::SYNTAX_ID => 4,
|
||||
SyntaxType::INDEX_ID => 5,
|
||||
SyntaxType::REFERENCE_UUID => 6,
|
||||
SyntaxType::JSON_FILTER => 7,
|
||||
SyntaxType::Credential => 8,
|
||||
SyntaxType::SecretUtf8String => 9,
|
||||
SyntaxType::SshKey => 10,
|
||||
SyntaxType::SecurityPrincipalName => 11,
|
||||
SyntaxType::UINT32 => 12,
|
||||
SyntaxType::Cid => 13,
|
||||
SyntaxType::Utf8StringIname => 14,
|
||||
SyntaxType::NsUniqueId => 15,
|
||||
SyntaxType::DateTime => 16,
|
||||
SyntaxType::EmailAddress => 17,
|
||||
SyntaxType::Url => 18,
|
||||
SyntaxType::OauthScope => 19,
|
||||
SyntaxType::OauthScopeMap => 20,
|
||||
SyntaxType::PrivateBinary => 21,
|
||||
SyntaxType::IntentToken => 22,
|
||||
SyntaxType::Passkey => 23,
|
||||
SyntaxType::DeviceKey => 24,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SyntaxType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
SyntaxType::UTF8STRING => "UTF8STRING",
|
||||
SyntaxType::Utf8String => "UTF8STRING",
|
||||
SyntaxType::Utf8StringInsensitive => "UTF8STRING_INSENSITIVE",
|
||||
SyntaxType::Utf8StringIname => "UTF8STRING_INAME",
|
||||
SyntaxType::Uuid => "UUID",
|
||||
SyntaxType::Boolean => "BOOLEAN",
|
||||
SyntaxType::SYNTAX_ID => "SYNTAX_ID",
|
||||
SyntaxType::INDEX_ID => "INDEX_ID",
|
||||
SyntaxType::REFERENCE_UUID => "REFERENCE_UUID",
|
||||
SyntaxType::JSON_FILTER => "JSON_FILTER",
|
||||
SyntaxType::SyntaxId => "SYNTAX_ID",
|
||||
SyntaxType::IndexId => "INDEX_ID",
|
||||
SyntaxType::ReferenceUuid => "REFERENCE_UUID",
|
||||
SyntaxType::JsonFilter => "JSON_FILTER",
|
||||
SyntaxType::Credential => "CREDENTIAL",
|
||||
SyntaxType::SecretUtf8String => "SECRET_UTF8STRING",
|
||||
SyntaxType::SshKey => "SSHKEY",
|
||||
SyntaxType::SecurityPrincipalName => "SECURITY_PRINCIPAL_NAME",
|
||||
SyntaxType::UINT32 => "UINT32",
|
||||
SyntaxType::Uint32 => "UINT32",
|
||||
SyntaxType::Cid => "CID",
|
||||
SyntaxType::NsUniqueId => "NSUNIQUEID",
|
||||
SyntaxType::DateTime => "DATETIME",
|
||||
|
@ -309,6 +289,9 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::IntentToken => "INTENT_TOKEN",
|
||||
SyntaxType::Passkey => "PASSKEY",
|
||||
SyntaxType::DeviceKey => "DEVICEKEY",
|
||||
SyntaxType::Session => "SESSION",
|
||||
SyntaxType::JwsKeyEs256 => "JWS_KEY_ES256",
|
||||
SyntaxType::JwsKeyRs256 => "JWS_KEY_RS256",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -347,7 +330,7 @@ pub enum PartialValue {
|
|||
// Can add other selectors later.
|
||||
Url(Url),
|
||||
OauthScope(String),
|
||||
OauthScopeMap(Uuid),
|
||||
// OauthScopeMap(Uuid),
|
||||
PrivateBinary,
|
||||
PublicBinary(String),
|
||||
// Enumeration(String),
|
||||
|
@ -359,7 +342,7 @@ pub enum PartialValue {
|
|||
DeviceKey(Uuid),
|
||||
|
||||
TrustedDeviceEnrollment(Uuid),
|
||||
AuthSession(Uuid),
|
||||
Session(Uuid),
|
||||
}
|
||||
|
||||
impl From<SyntaxType> for PartialValue {
|
||||
|
@ -645,6 +628,7 @@ impl PartialValue {
|
|||
matches!(self, PartialValue::OauthScope(_))
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn new_oauthscopemap(u: Uuid) -> Self {
|
||||
PartialValue::OauthScopeMap(u)
|
||||
}
|
||||
|
@ -659,6 +643,7 @@ impl PartialValue {
|
|||
pub fn is_oauthscopemap(&self) -> bool {
|
||||
matches!(self, PartialValue::OauthScopeMap(_))
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn is_privatebinary(&self) -> bool {
|
||||
matches!(self, PartialValue::PrivateBinary)
|
||||
|
@ -735,12 +720,11 @@ impl PartialValue {
|
|||
}
|
||||
PartialValue::Url(u) => u.to_string(),
|
||||
PartialValue::OauthScope(u) => u.to_string(),
|
||||
PartialValue::OauthScopeMap(u) => u.as_hyphenated().to_string(),
|
||||
PartialValue::Address(a) => a.to_string(),
|
||||
PartialValue::PhoneNumber(a) => a.to_string(),
|
||||
PartialValue::IntentToken(u) => u.clone(),
|
||||
PartialValue::TrustedDeviceEnrollment(u) => u.as_hyphenated().to_string(),
|
||||
PartialValue::AuthSession(u) => u.as_hyphenated().to_string(),
|
||||
PartialValue::Session(u) => u.as_hyphenated().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -749,6 +733,14 @@ impl PartialValue {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Session {
|
||||
pub label: String,
|
||||
pub expiry: Option<OffsetDateTime>,
|
||||
pub issued_at: OffsetDateTime,
|
||||
pub issued_by: IdentityId,
|
||||
}
|
||||
|
||||
/// A value is a complete unit of data for an attribute. It is made up of a PartialValue, which is
|
||||
/// used for selection, filtering, searching, matching etc. It also contains supplemental data
|
||||
/// which may be stored inside of the Value, such as credential secrets, blobs etc.
|
||||
|
@ -792,7 +784,10 @@ pub enum Value {
|
|||
DeviceKey(Uuid, String, DeviceKeyV4),
|
||||
|
||||
TrustedDeviceEnrollment(Uuid),
|
||||
AuthSession(Uuid),
|
||||
Session(Uuid, Session),
|
||||
|
||||
JwsKeyEs256(JwsSigner),
|
||||
JwsKeyRs256(JwsSigner),
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
|
@ -831,13 +826,16 @@ impl PartialEq for Value {
|
|||
// OauthScopeMap
|
||||
(Value::OauthScopeMap(a, c), Value::OauthScopeMap(b, d)) => a.eq(b) && c.eq(d),
|
||||
|
||||
// Address
|
||||
// PrivateBinary
|
||||
// SecretValue
|
||||
(Value::Address(_), Value::Address(_))
|
||||
| (Value::PrivateBinary(_), Value::PrivateBinary(_))
|
||||
| (Value::SecretValue(_), Value::SecretValue(_)) => false,
|
||||
_ => false,
|
||||
// Specifically related to migrations, we allow the invalid comparison.
|
||||
(Value::Iutf8(_), Value::Iname(_)) | (Value::Iname(_), Value::Iutf8(_)) => false,
|
||||
(l, r) => {
|
||||
error!(?l, ?r, "mismatched value types");
|
||||
debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1492,9 +1490,9 @@ impl Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_authsession(self) -> Option<(Uuid, ())> {
|
||||
pub fn to_session(self) -> Option<(Uuid, Session)> {
|
||||
match self {
|
||||
Value::AuthSession(u) => Some((u, ())),
|
||||
Value::Session(u, s) => Some((u, s)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -1584,7 +1582,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_value_syntax_tryfrom() {
|
||||
let r1 = SyntaxType::try_from("UTF8STRING");
|
||||
assert_eq!(r1, Ok(SyntaxType::UTF8STRING));
|
||||
assert_eq!(r1, Ok(SyntaxType::Utf8String));
|
||||
|
||||
let r2 = SyntaxType::try_from("UTF8STRING_INSENSITIVE");
|
||||
assert_eq!(r2, Ok(SyntaxType::Utf8StringInsensitive));
|
||||
|
@ -1593,10 +1591,10 @@ mod tests {
|
|||
assert_eq!(r3, Ok(SyntaxType::Boolean));
|
||||
|
||||
let r4 = SyntaxType::try_from("SYNTAX_ID");
|
||||
assert_eq!(r4, Ok(SyntaxType::SYNTAX_ID));
|
||||
assert_eq!(r4, Ok(SyntaxType::SyntaxId));
|
||||
|
||||
let r5 = SyntaxType::try_from("INDEX_ID");
|
||||
assert_eq!(r5, Ok(SyntaxType::INDEX_ID));
|
||||
assert_eq!(r5, Ok(SyntaxType::IndexId));
|
||||
|
||||
let r6 = SyntaxType::try_from("zzzzantheou");
|
||||
assert_eq!(r6, Err(()));
|
||||
|
|
|
@ -86,7 +86,7 @@ impl ValueSetT for ValueSetIndex {
|
|||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::INDEX_ID
|
||||
SyntaxType::IndexId
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
|
|
|
@ -96,7 +96,7 @@ impl ValueSetT for ValueSetJsonFilter {
|
|||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::JSON_FILTER
|
||||
SyntaxType::JsonFilter
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
|
|
319
kanidmd/idm/src/valueset/jws.rs
Normal file
319
kanidmd/idm/src/valueset/jws.rs
Normal file
|
@ -0,0 +1,319 @@
|
|||
use crate::prelude::*;
|
||||
use crate::schema::SchemaAttribute;
|
||||
use crate::valueset::DbValueSetV2;
|
||||
use crate::valueset::ValueSet;
|
||||
use hashbrown::HashSet;
|
||||
|
||||
use compact_jwt::{JwaAlg, JwsSigner};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetJwsKeyEs256 {
|
||||
set: HashSet<JwsSigner>,
|
||||
}
|
||||
|
||||
impl ValueSetJwsKeyEs256 {
|
||||
pub fn new(k: JwsSigner) -> Box<Self> {
|
||||
debug_assert!(k.get_jwa_alg() == JwaAlg::ES256);
|
||||
let mut set = HashSet::new();
|
||||
set.insert(k);
|
||||
Box::new(ValueSetJwsKeyEs256 { set })
|
||||
}
|
||||
|
||||
pub fn push(&mut self, k: JwsSigner) -> bool {
|
||||
debug_assert!(k.get_jwa_alg() == JwaAlg::ES256);
|
||||
self.set.insert(k)
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<Vec<u8>>) -> Result<ValueSet, OperationError> {
|
||||
let set = data
|
||||
.iter()
|
||||
.map(|b| {
|
||||
JwsSigner::from_es256_der(b).map_err(|e| {
|
||||
debug!(?e, "Error occured parsing ES256 DER");
|
||||
OperationError::InvalidValueState
|
||||
})
|
||||
})
|
||||
.collect::<Result<HashSet<_>, _>>()?;
|
||||
Ok(Box::new(ValueSetJwsKeyEs256 { set }))
|
||||
}
|
||||
|
||||
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
|
||||
// types, and jwssigner is foreign
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_iter<T>(iter: T) -> Option<Box<ValueSetJwsKeyEs256>>
|
||||
where
|
||||
T: IntoIterator<Item = JwsSigner>,
|
||||
{
|
||||
let set: HashSet<JwsSigner> = iter.into_iter().collect();
|
||||
debug_assert!(set.iter().all(|k| k.get_jwa_alg() == JwaAlg::ES256));
|
||||
Some(Box::new(ValueSetJwsKeyEs256 { set }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSetT for ValueSetJwsKeyEs256 {
|
||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||
match value {
|
||||
Value::JwsKeyEs256(k) => Ok(self.set.insert(k)),
|
||||
_ => {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.set.clear();
|
||||
}
|
||||
|
||||
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Iutf8(kid) => {
|
||||
let x = self.set.len();
|
||||
self.set.retain(|k| k.get_kid() != kid);
|
||||
x != self.set.len()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
|
||||
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||
self.set.iter().map(|k| k.get_kid().to_string()).collect()
|
||||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::JwsKeyEs256
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
self.set.iter().all(|k| k.get_jwa_alg() == JwaAlg::ES256)
|
||||
}
|
||||
|
||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||
Box::new(self.set.iter().map(|k| k.get_kid().to_string()))
|
||||
}
|
||||
|
||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||
DbValueSetV2::JwsKeyEs256(self.set.iter()
|
||||
.filter_map(|k| k.private_key_to_der()
|
||||
.map_err(|e| {
|
||||
error!(?e, "Unable to process private key to der, likely corrupted - this key will be LOST");
|
||||
})
|
||||
.ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
Box::new(
|
||||
self.set
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|k| PartialValue::new_iutf8(k.get_kid())),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||
Box::new(self.set.iter().cloned().map(Value::JwsKeyEs256))
|
||||
}
|
||||
|
||||
fn equal(&self, other: &ValueSet) -> bool {
|
||||
if let Some(other) = other.as_jws_key_es256_set() {
|
||||
&self.set == other
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||
if let Some(b) = other.as_jws_key_es256_set() {
|
||||
mergesets!(self.set, b)
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_jws_key_es256_single(&self) -> Option<&JwsSigner> {
|
||||
if self.set.len() == 1 {
|
||||
self.set.iter().take(1).next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_jws_key_es256_set(&self) -> Option<&HashSet<JwsSigner>> {
|
||||
Some(&self.set)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetJwsKeyRs256 {
|
||||
set: HashSet<JwsSigner>,
|
||||
}
|
||||
|
||||
impl ValueSetJwsKeyRs256 {
|
||||
pub fn new(k: JwsSigner) -> Box<Self> {
|
||||
debug_assert!(k.get_jwa_alg() == JwaAlg::RS256);
|
||||
let mut set = HashSet::new();
|
||||
set.insert(k);
|
||||
Box::new(ValueSetJwsKeyRs256 { set })
|
||||
}
|
||||
|
||||
pub fn push(&mut self, k: JwsSigner) -> bool {
|
||||
debug_assert!(k.get_jwa_alg() == JwaAlg::RS256);
|
||||
self.set.insert(k)
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<Vec<u8>>) -> Result<ValueSet, OperationError> {
|
||||
let set = data
|
||||
.iter()
|
||||
.map(|b| {
|
||||
JwsSigner::from_rs256_der(b).map_err(|e| {
|
||||
debug!(?e, "Error occured parsing RS256 DER");
|
||||
OperationError::InvalidValueState
|
||||
})
|
||||
})
|
||||
.collect::<Result<HashSet<_>, _>>()?;
|
||||
Ok(Box::new(ValueSetJwsKeyRs256 { set }))
|
||||
}
|
||||
|
||||
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
|
||||
// types, and jwssigner is foreign
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_iter<T>(iter: T) -> Option<Box<ValueSetJwsKeyRs256>>
|
||||
where
|
||||
T: IntoIterator<Item = JwsSigner>,
|
||||
{
|
||||
let set: HashSet<JwsSigner> = iter.into_iter().collect();
|
||||
debug_assert!(set.iter().all(|k| k.get_jwa_alg() == JwaAlg::RS256));
|
||||
Some(Box::new(ValueSetJwsKeyRs256 { set }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSetT for ValueSetJwsKeyRs256 {
|
||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||
match value {
|
||||
Value::JwsKeyRs256(k) => Ok(self.set.insert(k)),
|
||||
_ => {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.set.clear();
|
||||
}
|
||||
|
||||
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Iutf8(kid) => {
|
||||
let x = self.set.len();
|
||||
self.set.retain(|k| k.get_kid() != kid);
|
||||
x != self.set.len()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.set.len()
|
||||
}
|
||||
|
||||
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||
self.set.iter().map(|k| k.get_kid().to_string()).collect()
|
||||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::JwsKeyRs256
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
self.set.iter().all(|k| k.get_jwa_alg() == JwaAlg::RS256)
|
||||
}
|
||||
|
||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||
Box::new(self.set.iter().map(|k| k.get_kid().to_string()))
|
||||
}
|
||||
|
||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||
DbValueSetV2::JwsKeyRs256(self.set.iter()
|
||||
.filter_map(|k| k.private_key_to_der()
|
||||
.map_err(|e| {
|
||||
error!(?e, "Unable to process private key to der, likely corrupted - this key will be LOST");
|
||||
})
|
||||
.ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
Box::new(
|
||||
self.set
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|k| PartialValue::new_iutf8(k.get_kid())),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||
Box::new(self.set.iter().cloned().map(Value::JwsKeyRs256))
|
||||
}
|
||||
|
||||
fn equal(&self, other: &ValueSet) -> bool {
|
||||
if let Some(other) = other.as_jws_key_rs256_set() {
|
||||
&self.set == other
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||
if let Some(b) = other.as_jws_key_rs256_set() {
|
||||
mergesets!(self.set, b)
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_jws_key_rs256_single(&self) -> Option<&JwsSigner> {
|
||||
if self.set.len() == 1 {
|
||||
self.set.iter().take(1).next()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_jws_key_rs256_set(&self) -> Option<&HashSet<JwsSigner>> {
|
||||
Some(&self.set)
|
||||
}
|
||||
}
|
|
@ -6,12 +6,15 @@ use crate::schema::SchemaAttribute;
|
|||
use crate::be::dbvalue::DbValueSetV2;
|
||||
use crate::value::Address;
|
||||
use crate::value::IntentTokenState;
|
||||
use crate::value::Session;
|
||||
use compact_jwt::JwsSigner;
|
||||
|
||||
use kanidm_proto::v1::Filter as ProtoFilter;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use dyn_clone::DynClone;
|
||||
use hashbrown::HashSet;
|
||||
use smolset::SmolSet;
|
||||
// use std::fmt::Debug;
|
||||
|
||||
|
@ -30,10 +33,12 @@ mod iname;
|
|||
mod index;
|
||||
mod iutf8;
|
||||
mod json;
|
||||
mod jws;
|
||||
mod nsuniqueid;
|
||||
mod oauth;
|
||||
mod restricted;
|
||||
mod secret;
|
||||
mod session;
|
||||
mod spn;
|
||||
mod ssh;
|
||||
mod syntax;
|
||||
|
@ -52,10 +57,12 @@ pub use self::iname::ValueSetIname;
|
|||
pub use self::index::ValueSetIndex;
|
||||
pub use self::iutf8::ValueSetIutf8;
|
||||
pub use self::json::ValueSetJsonFilter;
|
||||
pub use self::jws::{ValueSetJwsKeyEs256, ValueSetJwsKeyRs256};
|
||||
pub use self::nsuniqueid::ValueSetNsUniqueId;
|
||||
pub use self::oauth::{ValueSetOauthScope, ValueSetOauthScopeMap};
|
||||
pub use self::restricted::ValueSetRestricted;
|
||||
pub use self::secret::ValueSetSecret;
|
||||
pub use self::session::ValueSetSession;
|
||||
pub use self::spn::ValueSetSpn;
|
||||
pub use self::ssh::ValueSetSshKey;
|
||||
pub use self::syntax::ValueSetSyntax;
|
||||
|
@ -465,6 +472,31 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
|
|||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn as_session_map(&self) -> Option<&BTreeMap<Uuid, Session>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn to_jws_key_es256_single(&self) -> Option<&JwsSigner> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn as_jws_key_es256_set(&self) -> Option<&HashSet<JwsSigner>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn to_jws_key_rs256_single(&self) -> Option<&JwsSigner> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
|
||||
fn as_jws_key_rs256_set(&self) -> Option<&HashSet<JwsSigner>> {
|
||||
debug_assert!(false);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ValueSet {
|
||||
|
@ -516,7 +548,16 @@ pub fn from_result_value_iter(
|
|||
Value::PublicBinary(t, b) => ValueSetPublicBinary::new(t, b),
|
||||
Value::IntentToken(u, s) => ValueSetIntentToken::new(u, s),
|
||||
Value::EmailAddress(a, _) => ValueSetEmailAddress::new(a),
|
||||
_ => return Err(OperationError::InvalidValueState),
|
||||
Value::PhoneNumber(_, _)
|
||||
| Value::Passkey(_, _, _)
|
||||
| Value::DeviceKey(_, _, _)
|
||||
| Value::TrustedDeviceEnrollment(_)
|
||||
| Value::Session(_, _)
|
||||
| Value::JwsKeyEs256(_)
|
||||
| Value::JwsKeyRs256(_) => {
|
||||
debug_assert!(false);
|
||||
return Err(OperationError::InvalidValueState);
|
||||
}
|
||||
};
|
||||
|
||||
for maybe_v in iter {
|
||||
|
@ -564,7 +605,13 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
|||
Value::EmailAddress(a, _) => ValueSetEmailAddress::new(a),
|
||||
Value::Passkey(u, t, k) => ValueSetPasskey::new(u, t, k),
|
||||
Value::DeviceKey(u, t, k) => ValueSetDeviceKey::new(u, t, k),
|
||||
_ => return Err(OperationError::InvalidValueState),
|
||||
Value::JwsKeyEs256(k) => ValueSetJwsKeyEs256::new(k),
|
||||
Value::JwsKeyRs256(k) => ValueSetJwsKeyRs256::new(k),
|
||||
Value::Session(u, m) => ValueSetSession::new(u, m),
|
||||
Value::PhoneNumber(_, _) | Value::TrustedDeviceEnrollment(_) => {
|
||||
debug_assert!(false);
|
||||
return Err(OperationError::InvalidValueState);
|
||||
}
|
||||
};
|
||||
|
||||
for v in iter {
|
||||
|
@ -603,11 +650,11 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
|
|||
DbValueSetV2::EmailAddress(primary, set) => ValueSetEmailAddress::from_dbvs2(primary, set),
|
||||
DbValueSetV2::Passkey(set) => ValueSetPasskey::from_dbvs2(set),
|
||||
DbValueSetV2::DeviceKey(set) => ValueSetDeviceKey::from_dbvs2(set),
|
||||
/*
|
||||
DbValueSetV2::PhoneNumber(set) =>
|
||||
DbValueSetV2::TrustedDeviceEnrollment(set) =>
|
||||
DbValueSetV2::AuthSession(set) =>
|
||||
*/
|
||||
_ => unimplemented!(),
|
||||
DbValueSetV2::Session(set) => ValueSetSession::from_dbvs2(set),
|
||||
DbValueSetV2::JwsKeyEs256(set) => ValueSetJwsKeyEs256::from_dbvs2(set),
|
||||
DbValueSetV2::JwsKeyRs256(set) => ValueSetJwsKeyEs256::from_dbvs2(set),
|
||||
DbValueSetV2::PhoneNumber(_, _) | DbValueSetV2::TrustedDeviceEnrollment(_) => {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,14 +214,14 @@ impl ValueSetT for ValueSetOauthScopeMap {
|
|||
|
||||
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::OauthScopeMap(u) | PartialValue::Refer(u) => self.map.remove(u).is_some(),
|
||||
PartialValue::Refer(u) => self.map.remove(u).is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::OauthScopeMap(u) | PartialValue::Refer(u) => self.map.contains_key(u),
|
||||
PartialValue::Refer(u) => self.map.contains_key(u),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +277,7 @@ impl ValueSetT for ValueSetOauthScopeMap {
|
|||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
Box::new(self.map.keys().cloned().map(PartialValue::OauthScopeMap))
|
||||
Box::new(self.map.keys().cloned().map(PartialValue::Refer))
|
||||
}
|
||||
|
||||
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||
|
|
236
kanidmd/idm/src/valueset/session.rs
Normal file
236
kanidmd/idm/src/valueset/session.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
use crate::be::dbvalue::{DbValueIdentityId, DbValueSession};
|
||||
use crate::identity::IdentityId;
|
||||
use crate::prelude::*;
|
||||
use crate::schema::SchemaAttribute;
|
||||
use crate::value::Session;
|
||||
use crate::valueset::DbValueSetV2;
|
||||
use crate::valueset::ValueSet;
|
||||
use std::collections::btree_map::Entry as BTreeEntry;
|
||||
use std::collections::BTreeMap;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::valueset::uuid_to_proto_string;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValueSetSession {
|
||||
map: BTreeMap<Uuid, Session>,
|
||||
}
|
||||
|
||||
impl ValueSetSession {
|
||||
pub fn new(u: Uuid, m: Session) -> Box<Self> {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(u, m);
|
||||
Box::new(ValueSetSession { map })
|
||||
}
|
||||
|
||||
pub fn push(&mut self, u: Uuid, m: Session) -> bool {
|
||||
self.map.insert(u, m).is_none()
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<DbValueSession>) -> Result<ValueSet, OperationError> {
|
||||
let map = data
|
||||
.into_iter()
|
||||
.filter_map(|dbv| {
|
||||
match dbv {
|
||||
DbValueSession::V1 {
|
||||
refer,
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
} => {
|
||||
// Convert things.
|
||||
let issued_at = OffsetDateTime::parse(issued_at, time::Format::Rfc3339)
|
||||
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
?e,
|
||||
"Invalidating session {} due to invalid issued_at timestamp",
|
||||
refer
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
// This is a bit annoying. In the case we can't parse the optional
|
||||
// expiry, we need to NOT return the session so that it's immediately
|
||||
// invalidated. To do this we have to invert some of the options involved
|
||||
// here.
|
||||
let expiry = expiry
|
||||
.map(|e_inner| {
|
||||
OffsetDateTime::parse(e_inner, time::Format::Rfc3339)
|
||||
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
|
||||
// We now have an
|
||||
// Option<Result<ODT, _>>
|
||||
})
|
||||
.transpose()
|
||||
// Result<Option<ODT>, _>
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
?e,
|
||||
"Invalidating session {} due to invalid expiry timestamp",
|
||||
refer
|
||||
)
|
||||
})
|
||||
// Option<Option<ODT>>
|
||||
.ok()?;
|
||||
|
||||
let issued_by = match issued_by {
|
||||
DbValueIdentityId::V1Internal => IdentityId::Internal,
|
||||
DbValueIdentityId::V1Uuid(u) => IdentityId::User(u),
|
||||
};
|
||||
|
||||
Some((
|
||||
refer,
|
||||
Session {
|
||||
label,
|
||||
expiry,
|
||||
issued_at,
|
||||
issued_by,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(Box::new(ValueSetSession { map }))
|
||||
}
|
||||
|
||||
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
|
||||
// types, and tuples are always foreign.
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn from_iter<T>(iter: T) -> Option<Box<Self>>
|
||||
where
|
||||
T: IntoIterator<Item = (Uuid, Session)>,
|
||||
{
|
||||
let map = iter.into_iter().collect();
|
||||
Some(Box::new(ValueSetSession { map }))
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSetT for ValueSetSession {
|
||||
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
|
||||
match value {
|
||||
Value::Session(u, m) => {
|
||||
if let BTreeEntry::Vacant(e) = self.map.entry(u) {
|
||||
e.insert(m);
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
_ => Err(OperationError::InvalidValueState),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
}
|
||||
|
||||
fn remove(&mut self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Refer(u) => self.map.remove(u).is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, pv: &PartialValue) -> bool {
|
||||
match pv {
|
||||
PartialValue::Refer(u) => self.map.contains_key(u),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn substring(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn lessthan(&self, _pv: &PartialValue) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
fn generate_idx_eq_keys(&self) -> Vec<String> {
|
||||
self.map
|
||||
.keys()
|
||||
.map(|u| u.as_hyphenated().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::Session
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||
Box::new(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, m)| format!("{}: {:?}", uuid_to_proto_string(*u), m)),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||
DbValueSetV2::Session(
|
||||
self.map
|
||||
.iter()
|
||||
.map(|(u, m)| DbValueSession::V1 {
|
||||
refer: *u,
|
||||
label: m.label.clone(),
|
||||
expiry: m.expiry.map(|odt| {
|
||||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
odt.format(time::Format::Rfc3339)
|
||||
}),
|
||||
issued_at: {
|
||||
debug_assert!(m.issued_at.offset() == time::UtcOffset::UTC);
|
||||
m.issued_at.format(time::Format::Rfc3339)
|
||||
},
|
||||
issued_by: match m.issued_by {
|
||||
IdentityId::Internal => DbValueIdentityId::V1Internal,
|
||||
IdentityId::User(u) => DbValueIdentityId::V1Uuid(u),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
Box::new(self.map.keys().cloned().map(PartialValue::Refer))
|
||||
}
|
||||
|
||||
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
|
||||
Box::new(self.map.iter().map(|(u, m)| Value::Session(*u, m.clone())))
|
||||
}
|
||||
|
||||
fn equal(&self, other: &ValueSet) -> bool {
|
||||
if let Some(other) = other.as_session_map() {
|
||||
&self.map == other
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
|
||||
if let Some(b) = other.as_session_map() {
|
||||
mergemaps!(self.map, b)
|
||||
} else {
|
||||
debug_assert!(false);
|
||||
Err(OperationError::InvalidValueState)
|
||||
}
|
||||
}
|
||||
|
||||
fn as_session_map(&self) -> Option<&BTreeMap<Uuid, Session>> {
|
||||
Some(&self.map)
|
||||
}
|
||||
|
||||
fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
|
||||
// This is what ties us as a type that can be refint checked.
|
||||
Some(Box::new(self.map.keys().copied()))
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ impl ValueSetSyntax {
|
|||
self.set.insert(s)
|
||||
}
|
||||
|
||||
pub fn from_dbvs2(data: Vec<usize>) -> Result<ValueSet, OperationError> {
|
||||
pub fn from_dbvs2(data: Vec<u16>) -> Result<ValueSet, OperationError> {
|
||||
let set: Result<_, _> = data.into_iter().map(SyntaxType::try_from).collect();
|
||||
let set = set.map_err(|()| OperationError::InvalidValueState)?;
|
||||
Ok(Box::new(ValueSetSyntax { set }))
|
||||
|
@ -86,7 +86,7 @@ impl ValueSetT for ValueSetSyntax {
|
|||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::SYNTAX_ID
|
||||
SyntaxType::SyntaxId
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
|
@ -98,7 +98,7 @@ impl ValueSetT for ValueSetSyntax {
|
|||
}
|
||||
|
||||
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
|
||||
DbValueSetV2::SyntaxType(self.set.iter().map(|s| s.to_usize()).collect())
|
||||
DbValueSetV2::SyntaxType(self.set.iter().map(|s| *s as u16).collect())
|
||||
}
|
||||
|
||||
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
|
||||
|
|
|
@ -89,7 +89,7 @@ impl ValueSetT for ValueSetUint32 {
|
|||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::UINT32
|
||||
SyntaxType::Uint32
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
|
|
|
@ -78,7 +78,7 @@ impl ValueSetT for ValueSetUtf8 {
|
|||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::UTF8STRING
|
||||
SyntaxType::Utf8String
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
|
|
|
@ -241,7 +241,7 @@ impl ValueSetT for ValueSetRefer {
|
|||
}
|
||||
|
||||
fn syntax(&self) -> SyntaxType {
|
||||
SyntaxType::REFERENCE_UUID
|
||||
SyntaxType::ReferenceUuid
|
||||
}
|
||||
|
||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||
|
|
|
@ -44,7 +44,7 @@ profiles = { path = "../../profiles" }
|
|||
kanidm_client = { path = "../../kanidm_client" }
|
||||
futures = "^0.3.21"
|
||||
|
||||
webauthn-authenticator-rs = "0.4.5"
|
||||
webauthn-authenticator-rs.workspace = true
|
||||
oauth2_ext = { package = "oauth2", version = "^4.1.0", default-features = false }
|
||||
base64 = "^0.13.0"
|
||||
|
||||
|
|
|
@ -73,6 +73,8 @@ pub trait RequestExtensions {
|
|||
|
||||
fn get_url_param(&self, param: &str) -> Result<String, tide::Error>;
|
||||
|
||||
fn get_url_param_uuid(&self, param: &str) -> Result<Uuid, tide::Error>;
|
||||
|
||||
fn new_eventid(&self) -> (Uuid, String);
|
||||
}
|
||||
|
||||
|
@ -92,16 +94,6 @@ impl RequestExtensions for tide::Request<AppState> {
|
|||
})
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| self.session().get::<String>("bearer"))
|
||||
/*
|
||||
.and_then(|ts| {
|
||||
// Take the token str and attempt to decrypt
|
||||
// Attempt to re-inflate a UAT from bytes.
|
||||
//
|
||||
// NOTE: UAT expiry validation is performed in event.rs!
|
||||
let uat: Option<UserAuthToken> = kref.verify(ts).ok();
|
||||
uat
|
||||
})
|
||||
*/
|
||||
}
|
||||
|
||||
fn get_current_auth_session_id(&self) -> Option<Uuid> {
|
||||
|
@ -119,7 +111,7 @@ impl RequestExtensions for tide::Request<AppState> {
|
|||
})
|
||||
.and_then(|jwsu| {
|
||||
jwsu.validate(kref)
|
||||
.map(|jws: Jws<SessionId>| jws.inner.sessionid)
|
||||
.map(|jws: Jws<SessionId>| jws.into_inner().sessionid)
|
||||
.ok()
|
||||
})
|
||||
// If not there, get from the cookie instead.
|
||||
|
@ -133,6 +125,20 @@ impl RequestExtensions for tide::Request<AppState> {
|
|||
})
|
||||
}
|
||||
|
||||
fn get_url_param_uuid(&self, param: &str) -> Result<Uuid, tide::Error> {
|
||||
self.param(param)
|
||||
.map_err(|e| {
|
||||
error!(?e);
|
||||
tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot")
|
||||
})
|
||||
.and_then(|s| {
|
||||
Uuid::try_parse(s).map_err(|e| {
|
||||
error!(?e);
|
||||
tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn new_eventid(&self) -> (Uuid, String) {
|
||||
let eventid = sketching::tracing_forest::id();
|
||||
let hv = eventid.as_hyphenated().to_string();
|
||||
|
@ -686,6 +692,14 @@ pub fn create_https_server(
|
|||
.at("/:id/_into_person")
|
||||
.mapped_post(&mut routemap, service_account_into_person);
|
||||
|
||||
service_account_route
|
||||
.at("/:id/_api_token")
|
||||
.mapped_post(&mut routemap, service_account_api_token_post)
|
||||
.mapped_get(&mut routemap, service_account_api_token_get);
|
||||
service_account_route
|
||||
.at("/:id/_api_token/:token_id")
|
||||
.mapped_delete(&mut routemap, service_account_api_token_delete);
|
||||
|
||||
service_account_route
|
||||
.at("/:id/_credential")
|
||||
.mapped_get(&mut routemap, do_nothing);
|
||||
|
|
|
@ -6,9 +6,9 @@ use kanidm::status::StatusRequestEvent;
|
|||
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::{
|
||||
AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState, CUIntentToken,
|
||||
CURequest, CUSessionToken, CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest,
|
||||
OperationError, SearchRequest, SingleStringRequest,
|
||||
AccountUnixExtend, ApiTokenGenerate, AuthRequest, AuthResponse, AuthState as ProtoAuthState,
|
||||
CUIntentToken, CURequest, CUSessionToken, CreateRequest, DeleteRequest, GroupUnixExtend,
|
||||
ModifyRequest, OperationError, SearchRequest, SingleStringRequest,
|
||||
};
|
||||
|
||||
use super::{to_tide_response, AppState, RequestExtensions, RouteMap};
|
||||
|
@ -420,6 +420,52 @@ pub async fn service_account_into_person(req: tide::Request<AppState>) -> tide::
|
|||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
// Api Token
|
||||
pub async fn service_account_api_token_get(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_service_account_api_token_get(uat, uuid_or_name, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn service_account_api_token_post(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let ApiTokenGenerate { label, expiry } = req.body_json().await?;
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_service_account_api_token_generate(uat, uuid_or_name, label, expiry, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn service_account_api_token_delete(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let token_id = req.get_url_param_uuid("token_id")?;
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_service_account_api_token_destroy(uat, uuid_or_name, token_id, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
// Account stuff
|
||||
pub async fn account_id_get_attr(req: tide::Request<AppState>) -> tide::Result {
|
||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
|
||||
json_rest_event_get_id_attr(req, filter).await
|
||||
|
@ -960,9 +1006,7 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
|
|||
.and_then(|_| {
|
||||
let kref = &req.state().jws_signer;
|
||||
|
||||
let jws = Jws {
|
||||
inner: SessionId { sessionid },
|
||||
};
|
||||
let jws = Jws::new(SessionId { sessionid });
|
||||
// Get the header token ready.
|
||||
jws.sign(&kref)
|
||||
.map(|jwss| {
|
||||
|
@ -989,9 +1033,7 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
|
|||
.and_then(|_| {
|
||||
let kref = &req.state().jws_signer;
|
||||
// Get the header token ready.
|
||||
let jws = Jws {
|
||||
inner: SessionId { sessionid },
|
||||
};
|
||||
let jws = Jws::new(SessionId { sessionid });
|
||||
jws.sign(&kref)
|
||||
.map(|jwss| {
|
||||
auth_session_id_tok = Some(jwss.to_string());
|
||||
|
|
|
@ -4,10 +4,14 @@ use std::time::SystemTime;
|
|||
use tracing::debug;
|
||||
|
||||
use kanidm::credential::totp::Totp;
|
||||
use kanidm_proto::v1::{CURegState, CredentialDetailType, Entry, Filter, Modify, ModifyList};
|
||||
use kanidm_proto::v1::{
|
||||
ApiToken, CURegState, CredentialDetailType, Entry, Filter, Modify, ModifyList,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use crate::common::{setup_async_test, ADMIN_TEST_PASSWORD};
|
||||
use compact_jwt::JwsUnverified;
|
||||
use std::str::FromStr;
|
||||
|
||||
use webauthn_authenticator_rs::{softpasskey::SoftPasskey, WebauthnAuthenticator};
|
||||
|
||||
|
@ -78,12 +82,13 @@ async fn test_server_whoami_anonymous() {
|
|||
assert!(res.is_ok());
|
||||
|
||||
// Now do a whoami.
|
||||
let (_e, uat) = match rsclient.whoami().await.unwrap() {
|
||||
Some((e, uat)) => (e, uat),
|
||||
None => panic!(),
|
||||
};
|
||||
debug!("{}", uat);
|
||||
assert!(uat.spn == "anonymous@localhost");
|
||||
let e = rsclient
|
||||
.whoami()
|
||||
.await
|
||||
.expect("Unable to call whoami")
|
||||
.expect("No entry matching self returned");
|
||||
debug!(?e);
|
||||
assert!(e.attrs.get("spn") == Some(&vec!["anonymous@localhost".to_string()]));
|
||||
|
||||
// Do a check of the auth/valid endpoint, tells us if our token
|
||||
// is okay.
|
||||
|
@ -105,12 +110,13 @@ async fn test_server_whoami_admin_simple_password() {
|
|||
assert!(res.is_ok());
|
||||
|
||||
// Now do a whoami.
|
||||
let (_e, uat) = match rsclient.whoami().await.unwrap() {
|
||||
Some((e, uat)) => (e, uat),
|
||||
None => panic!(),
|
||||
};
|
||||
debug!("{}", uat);
|
||||
assert!(uat.spn == "admin@localhost");
|
||||
let e = rsclient
|
||||
.whoami()
|
||||
.await
|
||||
.expect("Unable to call whoami")
|
||||
.expect("No entry matching self returned");
|
||||
debug!(?e);
|
||||
assert!(e.attrs.get("spn") == Some(&vec!["admin@localhost".to_string()]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -1164,3 +1170,62 @@ async fn test_server_credential_update_session_passkey() {
|
|||
let res = rsclient.auth_passkey_complete(pkc).await;
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_api_token_lifecycle() {
|
||||
let rsclient = setup_async_test().await;
|
||||
let res = rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", &["admin"])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
rsclient
|
||||
.idm_service_account_create("test_service", "Test Service")
|
||||
.await
|
||||
.expect("Failed to create service account");
|
||||
|
||||
let tokens = rsclient
|
||||
.idm_service_account_list_api_token("test_service")
|
||||
.await
|
||||
.expect("Failed to list service account api tokens");
|
||||
assert!(tokens.is_empty());
|
||||
|
||||
let token = rsclient
|
||||
.idm_service_account_generate_api_token("test_service", "test token", None)
|
||||
.await
|
||||
.expect("Failed to create service account api token");
|
||||
|
||||
// Decode it?
|
||||
let token_unverified = JwsUnverified::from_str(&token).expect("Failed to parse apitoken");
|
||||
|
||||
let token: ApiToken = token_unverified
|
||||
.validate_embeded()
|
||||
.map(|j| j.into_inner())
|
||||
.expect("Embedded jwk not found");
|
||||
|
||||
let tokens = rsclient
|
||||
.idm_service_account_list_api_token("test_service")
|
||||
.await
|
||||
.expect("Failed to list service account api tokens");
|
||||
|
||||
assert!(tokens == vec![token.clone()]);
|
||||
|
||||
rsclient
|
||||
.idm_service_account_destroy_api_token(&token.account_id.to_string(), token.token_id)
|
||||
.await
|
||||
.expect("Failed to destroy service account api token");
|
||||
|
||||
let tokens = rsclient
|
||||
.idm_service_account_list_api_token("test_service")
|
||||
.await
|
||||
.expect("Failed to list service account api tokens");
|
||||
assert!(tokens.is_empty());
|
||||
|
||||
// No need to test expiry, that's validated in the server internal tests.
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ authors = [
|
|||
"William Brown <william@blackhats.net.au>",
|
||||
"James Hodgkinson <james@terminaloutcomes.com>",
|
||||
]
|
||||
rust-version = "1.59"
|
||||
rust-version = "1.64"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "Kanidm Server Web User Interface"
|
||||
|
@ -33,7 +33,7 @@ serde = { version = "^1.0.142", features = ["derive"] }
|
|||
serde_json = "^1.0.83"
|
||||
serde-wasm-bindgen = "0.4"
|
||||
uuid = "^1.1.2"
|
||||
wasm-bindgen = { version = "^0.2.81", features = ["serde-serialize"] }
|
||||
wasm-bindgen = { version = "^0.2.81" }
|
||||
wasm-bindgen-futures = { version = "^0.4.30" }
|
||||
wasm-bindgen-test = "0.3.33"
|
||||
yew = "^0.19.3"
|
||||
|
|
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
@ -14,9 +14,11 @@
|
|||
"files": [
|
||||
"kanidmd_web_ui_bg.wasm",
|
||||
"kanidmd_web_ui.js",
|
||||
"kanidmd_web_ui.d.ts",
|
||||
"LICENSE.md"
|
||||
],
|
||||
"module": "kanidmd_web_ui.js",
|
||||
"homepage": "https://github.com/kanidm/kanidm/",
|
||||
"types": "kanidmd_web_ui.d.ts",
|
||||
"sideEffects": false
|
||||
}
|
|
@ -262,9 +262,9 @@ impl ChangeUnixPassword {
|
|||
|
||||
let uat: Jws<UserAuthToken> = jwtu
|
||||
.unsafe_release_without_verification()
|
||||
.expect_throw("Unvalid UAT, unable to release ");
|
||||
.expect_throw("Invalid UAT, unable to release ");
|
||||
|
||||
let id = uat.inner.uuid.to_string();
|
||||
let id = uat.into_inner().uuid.to_string();
|
||||
let changereq_jsvalue = serde_json::to_string(&SingleStringRequest {
|
||||
value: new_password,
|
||||
})
|
||||
|
|
|
@ -100,7 +100,9 @@ impl LoginApp {
|
|||
|
||||
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 resp: Response = resp_value
|
||||
.dyn_into()
|
||||
.expect_throw("Invalid response type - auth_init::Response");
|
||||
let status = resp.status();
|
||||
let headers = resp.headers();
|
||||
|
||||
|
@ -111,8 +113,8 @@ impl LoginApp {
|
|||
.flatten()
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let jsval = JsFuture::from(resp.json()?).await?;
|
||||
let state: AuthResponse =
|
||||
serde_wasm_bindgen::from_value(jsval).expect_throw("Invalid response type");
|
||||
let state: AuthResponse = serde_wasm_bindgen::from_value(jsval)
|
||||
.expect_throw("Invalid response type - auth_init::AuthResponse");
|
||||
Ok(LoginAppMsg::Start(session_id, state))
|
||||
} else if status == 404 {
|
||||
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||
|
@ -156,14 +158,20 @@ impl LoginApp {
|
|||
|
||||
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 resp: Response = resp_value
|
||||
.dyn_into()
|
||||
.expect_throw("Invalid response type - auth_step::Response");
|
||||
let status = resp.status();
|
||||
let headers = resp.headers();
|
||||
|
||||
if status == 200 {
|
||||
let jsval = JsFuture::from(resp.json()?).await?;
|
||||
let state: AuthResponse =
|
||||
serde_wasm_bindgen::from_value(jsval).expect_throw("Invalid response type.");
|
||||
let state: AuthResponse = serde_wasm_bindgen::from_value(jsval)
|
||||
.map_err(|e| {
|
||||
console::error!(format!("auth_step::AuthResponse: {:?}", e));
|
||||
e
|
||||
})
|
||||
.expect_throw("Invalid response type - auth_step::AuthResponse");
|
||||
Ok(LoginAppMsg::Next(state))
|
||||
} else {
|
||||
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||
|
|
|
@ -3,9 +3,10 @@ use crate::error::*;
|
|||
use crate::manager::Route;
|
||||
use crate::models;
|
||||
use crate::utils;
|
||||
use gloo::console;
|
||||
use kanidm_proto::v1::WhoamiResponse;
|
||||
use compact_jwt::{Jws, JwsUnverified};
|
||||
use kanidm_proto::v1::UserAuthToken;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||
|
@ -76,18 +77,20 @@ enum State {
|
|||
#[derive(PartialEq, Eq, Properties)]
|
||||
pub struct ViewProps {
|
||||
pub token: String,
|
||||
pub current_user: Option<WhoamiResponse>,
|
||||
// pub current_user_entry: Option<Entry>,
|
||||
pub current_user_uat: Option<UserAuthToken>,
|
||||
}
|
||||
|
||||
pub struct ViewsApp {
|
||||
state: State,
|
||||
current_user: Option<WhoamiResponse>,
|
||||
// pub current_user_entry: Option<Entry>,
|
||||
pub current_user_uat: Option<UserAuthToken>,
|
||||
}
|
||||
|
||||
pub enum ViewsMsg {
|
||||
Verified(String),
|
||||
Logout,
|
||||
ProfileInfoRecieved(WhoamiResponse),
|
||||
ProfileInfoRecieved { uat: UserAuthToken },
|
||||
Error { emsg: String, kopid: Option<String> },
|
||||
}
|
||||
|
||||
|
@ -128,7 +131,7 @@ impl Component for ViewsApp {
|
|||
|
||||
ViewsApp {
|
||||
state,
|
||||
current_user: None,
|
||||
current_user_uat: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,8 +162,8 @@ impl Component for ViewsApp {
|
|||
self.state = State::LoginRequired;
|
||||
true
|
||||
}
|
||||
ViewsMsg::ProfileInfoRecieved(profile) => {
|
||||
self.current_user = Some(profile);
|
||||
ViewsMsg::ProfileInfoRecieved { uat } => {
|
||||
self.current_user_uat = Some(uat);
|
||||
true
|
||||
}
|
||||
ViewsMsg::Error { emsg, kopid } => {
|
||||
|
@ -234,7 +237,7 @@ impl Component for ViewsApp {
|
|||
impl ViewsApp {
|
||||
/// The base page for the user dashboard
|
||||
fn view_authenticated(&self, ctx: &Context<Self>) -> Html {
|
||||
let current_user = self.current_user.clone();
|
||||
let current_user_uat = self.current_user_uat.clone();
|
||||
|
||||
// WARN set dash-body against body here?
|
||||
html! {
|
||||
|
@ -325,8 +328,8 @@ impl ViewsApp {
|
|||
<Switch<AdminRoute> render={ Switch::render(admin_routes) } />
|
||||
},
|
||||
ViewRoute::Apps => html! { <AppsApp /> },
|
||||
ViewRoute::Profile => html! { <ProfileApp token={ token } current_user={ current_user.clone() } /> },
|
||||
ViewRoute::Security => html! { <SecurityApp token={ token } current_user={ current_user.clone() } /> },
|
||||
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::NotFound => html! {
|
||||
<Redirect<Route> to={Route::NotFound}/>
|
||||
},
|
||||
|
@ -370,42 +373,20 @@ impl ViewsApp {
|
|||
Ok(ViewsMsg::Error { emsg, kopid })
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_user_data(token: String) -> Result<ViewsMsg, FetchError> {
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method("GET");
|
||||
opts.mode(RequestMode::SameOrigin);
|
||||
let jwtu = JwsUnverified::from_str(&token).expect_throw("Invalid UAT, unable to parse");
|
||||
|
||||
let request = Request::new_with_str_and_init("/v1/self", &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 uat: Jws<UserAuthToken> = jwtu
|
||||
.unsafe_release_without_verification()
|
||||
.expect_throw("Invalid UAT, unable to release");
|
||||
|
||||
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();
|
||||
let headers = resp.headers();
|
||||
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||
|
||||
if status == 200 {
|
||||
let jsval = JsFuture::from(resp.json()?).await?;
|
||||
let whoamiresponse: WhoamiResponse = serde_wasm_bindgen::from_value(jsval)
|
||||
.map_err(|e| {
|
||||
let e_msg = format!("serde error getting user data -> {:?}", e);
|
||||
console::error!(e_msg.as_str());
|
||||
})
|
||||
.expect_throw("Invalid response type");
|
||||
Ok(ViewsMsg::ProfileInfoRecieved(whoamiresponse))
|
||||
} else {
|
||||
let text = JsFuture::from(resp.text()?).await?;
|
||||
let emsg = text.as_string().unwrap_or_else(|| "".to_string());
|
||||
Ok(ViewsMsg::Error { emsg, kopid })
|
||||
}
|
||||
// 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ impl Component for ProfileApp {
|
|||
fn changed(&mut self, ctx: &Context<Self>) -> bool {
|
||||
console::debug!(format!(
|
||||
"views::profile::changed current_user: {:?}",
|
||||
ctx.props().current_user,
|
||||
ctx.props().current_user_uat,
|
||||
));
|
||||
true
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ impl Component for ProfileApp {
|
|||
fn update(&mut self, ctx: &Context<Self>, _msg: Self::Message) -> bool {
|
||||
console::debug!(format!(
|
||||
"views::profile::update current_user: {:?}",
|
||||
ctx.props().current_user,
|
||||
ctx.props().current_user_uat,
|
||||
));
|
||||
true
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ impl Component for ProfileApp {
|
|||
|
||||
/// UI view for the user profile
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let pagecontent = match &ctx.props().current_user {
|
||||
let pagecontent = match &ctx.props().current_user_uat {
|
||||
None => {
|
||||
html! {
|
||||
<h2>
|
||||
|
@ -50,8 +50,8 @@ impl Component for ProfileApp {
|
|||
</h2>
|
||||
}
|
||||
}
|
||||
Some(userinfo) => {
|
||||
let mail_primary = match userinfo.uat.mail_primary.as_ref() {
|
||||
Some(uat) => {
|
||||
let mail_primary = match uat.mail_primary.as_ref() {
|
||||
Some(email_address) => {
|
||||
html! {
|
||||
<a href={ format!("mailto:{}", &email_address)}>
|
||||
|
@ -62,12 +62,19 @@ impl Component for ProfileApp {
|
|||
None => html! { {"<primary email is unset>"}},
|
||||
};
|
||||
|
||||
let spn = &userinfo.uat.spn.to_owned();
|
||||
let spn = &uat.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 = userinfo.uat.displayname.to_owned();
|
||||
let user_groups = userinfo.youare.attrs.get("memberof");
|
||||
let display_name = uat.displayname.to_owned();
|
||||
let user_groups: Vec<String> = uat
|
||||
.groups
|
||||
.iter()
|
||||
.map(|group| {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
group.spn.split('@').next().unwrap().to_string()
|
||||
})
|
||||
.collect();
|
||||
|
||||
html! {
|
||||
<dl class="row">
|
||||
|
@ -81,22 +88,18 @@ impl Component for ProfileApp {
|
|||
<dd class="col">
|
||||
<ul class="list-group">
|
||||
{
|
||||
match user_groups {
|
||||
Some(grouplist) => html!{
|
||||
{
|
||||
for grouplist.iter()
|
||||
.map(|group|
|
||||
{
|
||||
html!{ <li>{
|
||||
#[allow(clippy::unwrap_used)]
|
||||
group.split('@').next().unwrap().to_string()
|
||||
}</li> }
|
||||
|
||||
})
|
||||
}
|
||||
},
|
||||
None => html!{
|
||||
<li>{"Not a member of any groups"}</li>
|
||||
if user_groups.is_empty() {
|
||||
html!{
|
||||
<li>{"Not a member of any groups"}</li>
|
||||
}
|
||||
} else {
|
||||
html!{
|
||||
{
|
||||
for user_groups.iter()
|
||||
.map(|group|
|
||||
html!{ <li>{ group }</li> }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,7 +118,7 @@ impl Component for ProfileApp {
|
|||
{ "User's UUID" }
|
||||
</dt>
|
||||
<dd class="col">
|
||||
{ format!("{}", &userinfo.uat.uuid ) }
|
||||
{ format!("{}", &uat.uuid ) }
|
||||
</dd>
|
||||
|
||||
</dl>
|
||||
|
|
|
@ -14,7 +14,7 @@ use std::str::FromStr;
|
|||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use kanidm_proto::v1::{CUSessionToken, CUStatus, UserAuthToken};
|
||||
use kanidm_proto::v1::{CUSessionToken, CUStatus, UiHint, UserAuthToken};
|
||||
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
@ -86,7 +86,7 @@ impl Component for SecurityApp {
|
|||
.unsafe_release_without_verification()
|
||||
.expect_throw("Unvalid UAT, unable to release ");
|
||||
|
||||
let id = uat.inner.uuid.to_string();
|
||||
let id = uat.into_inner().uuid.to_string();
|
||||
|
||||
ctx.link().send_future(async {
|
||||
match Self::fetch_token_valid(id, token).await {
|
||||
|
@ -145,7 +145,7 @@ impl Component for SecurityApp {
|
|||
_ => html! { <></> },
|
||||
};
|
||||
|
||||
let current_user = ctx.props().current_user.clone();
|
||||
let current_user_uat = ctx.props().current_user_uat.clone();
|
||||
|
||||
html! {
|
||||
<>
|
||||
|
@ -169,8 +169,8 @@ impl Component for SecurityApp {
|
|||
</p>
|
||||
</div>
|
||||
<hr/>
|
||||
if let Some(user) = current_user {
|
||||
if user.youare.attrs.get("class").map(|x| x.contains(&String::from("posixaccount"))).unwrap_or(true) {
|
||||
if let Some(uat) = current_user_uat {
|
||||
if uat.ui_hints.contains(&UiHint::PosixAccount) {
|
||||
<div>
|
||||
<p>
|
||||
<ChangeUnixPassword token={ctx.props().token.clone()}></ChangeUnixPassword>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "profiles"
|
||||
version = "1.1.0-alpha.9"
|
||||
authors = ["William Brown <william@blackhats.net.au>"]
|
||||
rust-version = "1.59"
|
||||
rust-version = "1.64"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "Kanidm Build System Profiles"
|
||||
|
|
Loading…
Reference in a new issue