20220911 api tokens (#1071)

This commit is contained in:
Firstyear 2022-09-25 11:21:30 +10:00 committed by GitHub
parent ad468f0dfa
commit 082464f786
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 3483 additions and 1354 deletions

201
Cargo.lock generated
View file

@ -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",

View file

@ -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

View 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

View 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

View file

@ -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.

View file

@ -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"] }

View file

@ -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

View file

@ -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
}
}

View file

@ -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"

View file

@ -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 }
}
}

View file

@ -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"

View file

@ -2,6 +2,7 @@
use std::env;
use std::path::PathBuf;
use uuid::Uuid;
use clap::{CommandFactory, Parser};
use clap_complete::{generate_to, Shell};

View file

@ -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);
}
};

View file

@ -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.");

View file

@ -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;

View file

@ -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()

View file

@ -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 {

View file

@ -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"

View file

@ -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" }

View file

@ -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>,
(),
>
}
}

View file

@ -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(&lte)
}
#[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

View file

@ -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(&gte, 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,

View file

@ -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);

View file

@ -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(),
}
}

View file

@ -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>,

View file

@ -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"
]
}
}"#;

View file

@ -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"]
}
}"#;

View file

@ -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;

View file

@ -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"

View file

@ -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.

View file

@ -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> {

View file

@ -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,
}
}
}

View file

@ -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.

View file

@ -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)]

View file

@ -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(),
})
}

View file

@ -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(

View file

@ -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

View file

@ -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 })
}
}

View file

@ -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(),
}
}

View file

@ -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};

View file

@ -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

View file

@ -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");
}
)

View 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(&gte.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
&gte.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(&gte, 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());
});
}
}

View file

@ -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(&gte, 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),
};
}
)
}

View file

@ -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)]

View file

@ -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>>,

View file

@ -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))
}
}

View file

@ -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))

View file

@ -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 _;

View file

@ -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,

View file

@ -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(()));

View file

@ -86,7 +86,7 @@ impl ValueSetT for ValueSetIndex {
}
fn syntax(&self) -> SyntaxType {
SyntaxType::INDEX_ID
SyntaxType::IndexId
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {

View file

@ -96,7 +96,7 @@ impl ValueSetT for ValueSetJsonFilter {
}
fn syntax(&self) -> SyntaxType {
SyntaxType::JSON_FILTER
SyntaxType::JsonFilter
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {

View 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)
}
}

View file

@ -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!()
}
}
}

View file

@ -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> + '_> {

View 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()))
}
}

View file

@ -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> + '_> {

View file

@ -89,7 +89,7 @@ impl ValueSetT for ValueSetUint32 {
}
fn syntax(&self) -> SyntaxType {
SyntaxType::UINT32
SyntaxType::Uint32
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {

View file

@ -78,7 +78,7 @@ impl ValueSetT for ValueSetUtf8 {
}
fn syntax(&self) -> SyntaxType {
SyntaxType::UTF8STRING
SyntaxType::Utf8String
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {

View file

@ -241,7 +241,7 @@ impl ValueSetT for ValueSetRefer {
}
fn syntax(&self) -> SyntaxType {
SyntaxType::REFERENCE_UUID
SyntaxType::ReferenceUuid
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {

View file

@ -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"

View file

@ -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);

View file

@ -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());

View file

@ -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.
}

View file

@ -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

View file

@ -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
}

View file

@ -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,
})

View file

@ -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();

View file

@ -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(),
})
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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"