Begin the basis of the key provider model (#2640)

This completely reworks how we approach and handle cryptographic keys in Kanidm. This is needed as a foundation for replication coordination which will require handling and rotation of cryptographic keys in automated ways. 

This change influences many other parts of the code base in it's implementation.

The primary influences are:

* Modification of how domain user signing keys are revoked or rotated.
* Merging of all existing service-account token keys are retired (retained) keys into the domain to simplify token signing and validation
* Allowing multiple configurations of local command line tools to swap between instances using disparate signing keys.
* Modification of key retrieval to be key id based (KID), removing the need to embed the JWK into tokens

A side effect of this change is that most user authentication sessions and oauth2 sessions will have to be re-established after upgrade. However we feel that session renewal after upgrade is an expected side effect of an upgrade. 

In the future this lays the ground work to remove a large number of legacy key handling processes that have evolved, which will allow large parts of code to be removed.
This commit is contained in:
Firstyear 2024-04-16 09:44:37 +10:00 committed by GitHub
parent dfac06608a
commit d7834b52e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 5854 additions and 1790 deletions

303
Cargo.lock generated
View file

@ -44,9 +44,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
@ -128,9 +128,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
[[package]]
name = "anymap2"
@ -140,9 +140,9 @@ checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
[[package]]
name = "arc-swap"
version = "1.7.0"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3d0060af21e8d11a926981cc00c6c1541aa91dd64b9f881985c3da1094425f"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "argon2"
@ -212,9 +212,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.4.6"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c"
checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
dependencies = [
"flate2",
"futures-core",
@ -231,7 +231,7 @@ checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -253,18 +253,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
name = "async-trait"
version = "0.1.78"
version = "0.1.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85"
checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -297,9 +297,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
[[package]]
name = "axum"
@ -388,7 +388,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -411,9 +411,9 @@ dependencies = [
[[package]]
name = "backtrace"
version = "0.3.69"
version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
dependencies = [
"addr2line",
"cc",
@ -486,13 +486,13 @@ dependencies = [
"lazycell",
"log",
"peeking_take_while",
"prettyplease 0.2.16",
"prettyplease 0.2.17",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.53",
"syn 2.0.58",
"which",
]
@ -509,13 +509,13 @@ dependencies = [
"lazy_static",
"lazycell",
"log",
"prettyplease 0.2.16",
"prettyplease 0.2.17",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.53",
"syn 2.0.58",
"which",
]
@ -619,9 +619,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.15.4"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "byte-tools"
@ -649,9 +649,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cast"
@ -661,9 +661,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.0.90"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
[[package]]
name = "cexpr"
@ -694,9 +694,9 @@ checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919"
[[package]]
name = "chrono"
version = "0.4.35"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
dependencies = [
"android-tzdata",
"iana-time-zone",
@ -731,7 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half 2.4.0",
"half 2.4.1",
]
[[package]]
@ -747,9 +747,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.3"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
@ -764,28 +764,28 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.0",
"strsim 0.11.1",
]
[[package]]
name = "clap_complete"
version = "4.5.1"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c"
checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.5.3"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -831,14 +831,14 @@ dependencies = [
[[package]]
name = "compact_jwt"
version = "0.3.5"
version = "0.4.0-dev"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7016ef64472942da7550eaf46657411e839d96dd88aba0dcef6b6cdc4a89766c"
checksum = "1d7adf14f5a7fc546b0828c69abe4dc4e16aad4de49e01553f54d6cbbc8a0a99"
dependencies = [
"base64 0.21.7",
"base64urlsafedata",
"hex",
"kanidm-hsm-crypto",
"kanidm-hsm-crypto 0.2.0",
"openssl",
"serde",
"serde_json",
@ -1176,7 +1176,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -1198,7 +1198,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core 0.20.8",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -1359,7 +1359,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -1394,9 +1394,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.33"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [
"cfg-if",
]
@ -1418,7 +1418,7 @@ checksum = "03cdc46ec28bd728e67540c528013c6a10eb69a02eb31078a1bda695438cbfb8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -1438,7 +1438,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -1539,9 +1539,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
[[package]]
name = "fernet"
@ -1719,7 +1719,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -1783,9 +1783,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.12"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [
"cfg-if",
"js-sys",
@ -2039,7 +2039,7 @@ checksum = "1dff438f14e67e7713ab9332f5fd18c8f20eb7eb249494f6c2bf170522224032"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -2481,7 +2481,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 2.2.5",
"indexmap 2.2.6",
"slab",
"tokio",
"tokio-util",
@ -2496,9 +2496,9 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403"
[[package]]
name = "half"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
@ -2804,9 +2804,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.2.5"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
@ -2888,9 +2888,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.10"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jpeg-decoder"
@ -2953,6 +2953,20 @@ dependencies = [
"zeroize",
]
[[package]]
name = "kanidm-hsm-crypto"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10b3ed8e86cda3da4f274c677a3057d567bd7b715a0feb06a656e55cc75faf5e"
dependencies = [
"argon2",
"hex",
"openssl",
"serde",
"tracing",
"zeroize",
]
[[package]]
name = "kanidm-ipa-sync"
version = "1.2.0-dev"
@ -3018,6 +3032,7 @@ dependencies = [
name = "kanidm_client"
version = "1.2.0-dev"
dependencies = [
"compact_jwt 0.4.0-dev",
"hyper",
"kanidm_lib_file_permissions",
"kanidm_proto",
@ -3041,7 +3056,7 @@ dependencies = [
"base64 0.21.7",
"base64urlsafedata",
"hex",
"kanidm-hsm-crypto",
"kanidm-hsm-crypto 0.1.6",
"kanidm_proto",
"openssl",
"openssl-sys",
@ -3087,7 +3102,7 @@ dependencies = [
"async-recursion",
"clap",
"clap_complete",
"compact_jwt 0.3.5",
"compact_jwt 0.4.0-dev",
"dialoguer",
"futures-concurrency",
"kanidm_build_profiles",
@ -3124,7 +3139,7 @@ dependencies = [
"csv",
"futures",
"hashbrown 0.14.3",
"kanidm-hsm-crypto",
"kanidm-hsm-crypto 0.1.6",
"kanidm_build_profiles",
"kanidm_client",
"kanidm_lib_crypto",
@ -3172,7 +3187,7 @@ dependencies = [
"axum-server",
"bytes",
"chrono",
"compact_jwt 0.3.5",
"compact_jwt 0.4.0-dev",
"cron",
"filetime",
"futures",
@ -3219,7 +3234,8 @@ version = "1.2.0-dev"
dependencies = [
"base64 0.21.7",
"base64urlsafedata",
"compact_jwt 0.3.5",
"bitflags 2.5.0",
"compact_jwt 0.4.0-dev",
"concread",
"criterion",
"dyn-clone",
@ -3277,7 +3293,7 @@ version = "1.2.0-dev"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -3285,7 +3301,7 @@ name = "kanidmd_testkit"
version = "1.2.0-dev"
dependencies = [
"assert_cmd",
"compact_jwt 0.3.5",
"compact_jwt 0.4.0-dev",
"escargot",
"fantoccini",
"futures",
@ -3471,9 +3487,9 @@ dependencies = [
[[package]]
name = "ldap3_proto"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a29eca0a9fef365d6d376a1b262e269a17b1c8c6de2cee76618642cd3c923506"
checksum = "9a35c5ce9e52b4e5b333422203a266e4466ed8b43768c877c1d3d23bf2b4d561"
dependencies = [
"base64 0.21.7",
"bytes",
@ -3516,13 +3532,12 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.0.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.5.0",
"libc",
"redox_syscall 0.4.1",
]
[[package]]
@ -3641,9 +3656,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.7.1"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "memmap2"
@ -4062,7 +4077,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -4073,9 +4088,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.101"
version = "0.9.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
dependencies = [
"cc",
"libc",
@ -4374,7 +4389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
"indexmap 2.2.5",
"indexmap 2.2.6",
"serde",
"serde_derive",
]
@ -4431,14 +4446,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pin-utils"
@ -4552,12 +4567,12 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.16"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5"
checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7"
dependencies = [
"proc-macro2",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -4689,9 +4704,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "1.0.35"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
@ -4728,9 +4743,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
@ -4766,9 +4781,9 @@ dependencies = [
[[package]]
name = "redox_users"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
dependencies = [
"getrandom",
"libredox",
@ -4777,20 +4792,20 @@ dependencies = [
[[package]]
name = "reference-counted-singleton"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ffdf83b0d36b33b2a82a8993af7e72a6a9b601e83c5c343c822fff37dbc0860"
checksum = "242f841f006fa4f35979f74147f6d0be4402c19ca25b62b1c8e4c02e28288cb9"
[[package]]
name = "regex"
version = "1.10.3"
version = "1.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.6",
"regex-syntax 0.8.2",
"regex-syntax 0.8.3",
]
[[package]]
@ -4810,7 +4825,7 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.2",
"regex-syntax 0.8.3",
]
[[package]]
@ -4821,15 +4836,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]]
name = "reqwest"
version = "0.11.26"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"async-compression",
"base64 0.21.7",
@ -4957,7 +4972,7 @@ dependencies = [
"quote",
"rust-embed-utils",
"shellexpand 3.1.0",
"syn 2.0.53",
"syn 2.0.58",
"walkdir",
]
@ -4994,9 +5009,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.31"
version = "0.38.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
dependencies = [
"bitflags 2.5.0",
"errno",
@ -5016,9 +5031,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
[[package]]
name = "ryu"
@ -5081,9 +5096,9 @@ checksum = "621e3680f3e07db4c9c2c3fb07c6223ab2fab2e54bd3c04c3ae037990f428c32"
[[package]]
name = "security-framework"
version = "2.9.2"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
@ -5094,9 +5109,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.9.1"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [
"core-foundation-sys",
"libc",
@ -5104,9 +5119,9 @@ dependencies = [
[[package]]
name = "selinux"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c88696d7211f03e87034e8687498f3f71890633e0e3e0c051ca3a716d2bc03e4"
checksum = "53371b1e9bbbfffd65e5ac3c895c786ec35b7695bdc4a67a8b08c29c8d057e0b"
dependencies = [
"bitflags 2.5.0",
"libc",
@ -5118,9 +5133,9 @@ dependencies = [
[[package]]
name = "selinux-sys"
version = "0.6.8"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6d6e616814290fe172d6514bebd9b723733ba7d68e1ab74d341a90b99a36bb4"
checksum = "89d45498373dc17ec8ebb72e1fd320c015647b0157fc81dddf678e2e00205fec"
dependencies = [
"bindgen 0.69.4",
"cc",
@ -5202,14 +5217,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
name = "serde_json"
version = "1.0.114"
version = "1.0.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
dependencies = [
"itoa",
"ryu",
@ -5248,7 +5263,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.2.5",
"indexmap 2.2.6",
"serde",
"serde_derive",
"serde_json",
@ -5265,7 +5280,7 @@ dependencies = [
"darling 0.20.8",
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -5385,9 +5400,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.13.1"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
dependencies = [
"serde",
]
@ -5476,9 +5491,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
@ -5505,9 +5520,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.53"
version = "2.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
dependencies = [
"proc-macro2",
"quote",
@ -5583,7 +5598,7 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -5603,7 +5618,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -5638,9 +5653,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.34"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
@ -5661,9 +5676,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.17"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
@ -5696,9 +5711,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.36.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",
@ -5730,7 +5745,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -5802,7 +5817,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.2.5",
"indexmap 2.2.6",
"toml_datetime",
"winnow",
]
@ -5915,7 +5930,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -6144,7 +6159,7 @@ version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7"
dependencies = [
"indexmap 2.2.5",
"indexmap 2.2.6",
"serde",
"serde_json",
"utoipa-gen",
@ -6160,7 +6175,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.53",
"syn 2.0.58",
"url",
"uuid",
]
@ -6272,7 +6287,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
"wasm-bindgen-shared",
]
@ -6306,7 +6321,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -6339,7 +6354,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -6863,7 +6878,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]
@ -6883,7 +6898,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.53",
"syn 2.0.58",
]
[[package]]

View file

@ -111,12 +111,13 @@ axum-csp = { version = "0.0.5" }
base32 = "^0.4.0"
base64 = "^0.21.7"
base64urlsafedata = "0.1.3"
bitflags = "^2.4.2"
bytes = "^1.5.0"
clap = { version = "^4.5.3", features = ["derive", "env"] }
clap_complete = "^4.5.1"
# Forced by saffron/cron
chrono = "^0.4.35"
compact_jwt = { version = "^0.3.5", default-features = false }
compact_jwt = { version = "^0.4.0-dev", default-features = false }
concread = "^0.5.0"
cron = "0.12.1"
crossbeam = "0.8.4"

View file

@ -52,6 +52,8 @@ In general Kanidm requires that your resource server supports:
- PKCE S256 code verification
- OIDC only - JWT ES256 for token signatures
Kanidm issues tokens that are rfc9068 JWT's allowing client introspection.
Kanidm will expose its OAuth2 APIs at the following URLs:
- user auth url: `https://idm.example.com/ui/oauth2`

View file

@ -46,6 +46,7 @@ This is a list of supported features and standards within Kanidm.
- [RFC7009 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009)
- [RFC7636 Proof Key for Code Exchange (SHA256 Only)](https://www.rfc-editor.org/rfc/rfc7636)
- [RFC8414 OAuth 2.0 Authorisation Server Metadata](https://www.rfc-editor.org/rfc/rfc8414)
- [RFC9068 OAuth 2.0 JWT Access Tokens](https://www.rfc-editor.org/rfc/rfc9068)
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
- RBAC claim and scope mapping
- PII scope claim requests

View file

@ -2,3 +2,7 @@
uri = "https://idm.example.com"
verify_ca = true
verify_hostnames = true
# Alternate instances can be specified and used with -I <name> to the cli tools.
["name"]
uri = "https://alternate.example.com"

View file

@ -9,7 +9,7 @@ tls_client_ca = "/tmp/kanidm/client_ca"
# The log level of the server. May be one of info, debug, trace
#
# NOTE: this is overridden by environment variables at runtime
# NOTE: this is overridden by KANIDM_LOG_LEVEL environment variable
# Defaults to "info"
#
log_level = "info"

View file

@ -16,6 +16,7 @@ test = true
doctest = false
[dependencies]
compact_jwt = { workspace = true }
tracing = { workspace = true }
reqwest = { workspace = true, default-features = false, features = [
"cookies",

View file

@ -24,6 +24,7 @@ use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::time::Duration;
use compact_jwt::Jwk;
use kanidm_proto::constants::uri::V1_AUTH_VALID;
use kanidm_proto::constants::{
APPLICATION_JSON, ATTR_ENTRY_MANAGED_BY, ATTR_NAME, CLIENT_TOKEN_CACHE, KOPID, KSESSIONID,
@ -73,18 +74,9 @@ pub enum ClientError {
UntrustedCertificate(String),
}
/// Settings describing a single instance.
#[derive(Debug, Deserialize, Serialize)]
/// This struct is what Kanidm uses for parsing the client configuration at runtime.
///
/// # Configuration file inheritance
///
/// The configuration files are loaded in order, with the last one loaded overriding the previous one.
///
/// 1. The "system" config is loaded from in [kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH].
/// 2. Then a per-user configuration, from [kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH_HOME] is loaded.
/// 3. All of these may be overridden by setting environment variables.
///
pub struct KanidmClientConfig {
pub struct KanidmClientConfigInstance {
/// The URL of the server, ie `https://example.com`.
///
/// Environment variable is `KANIDM_URL`. Yeah, we know.
@ -103,6 +95,25 @@ pub struct KanidmClientConfig {
pub ca_path: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
/// This struct is what Kanidm uses for parsing the client configuration at runtime.
///
/// # Configuration file inheritance
///
/// The configuration files are loaded in order, with the last one loaded overriding the previous one.
///
/// 1. The "system" config is loaded from in [kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH].
/// 2. Then a per-user configuration, from [kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH_HOME] is loaded.
/// 3. All of these may be overridden by setting environment variables.
///
pub struct KanidmClientConfig {
#[serde(flatten)]
default: KanidmClientConfigInstance,
#[serde(flatten)]
instances: BTreeMap<String, KanidmClientConfigInstance>,
}
#[derive(Debug, Clone, Default)]
pub struct KanidmClientBuilder {
address: Option<String>,
@ -249,7 +260,7 @@ impl KanidmClientBuilder {
})
}
fn apply_config_options(self, kcc: KanidmClientConfig) -> Result<Self, ClientError> {
fn apply_config_options(self, kcc: KanidmClientConfigInstance) -> Result<Self, ClientError> {
let KanidmClientBuilder {
address,
verify_ca,
@ -289,7 +300,19 @@ impl KanidmClientBuilder {
self,
config_path: P,
) -> Result<Self, ClientError> {
debug!("Attempting to load configuration from {:#?}", &config_path);
self.read_options_from_optional_instance_config(config_path, None)
}
pub fn read_options_from_optional_instance_config<P: AsRef<Path> + std::fmt::Debug>(
self,
config_path: P,
instance: Option<&str>,
) -> Result<Self, ClientError> {
debug!(
"Attempting to load {} instance configuration from {:#?}",
instance.unwrap_or("default"),
&config_path
);
// We have to check the .exists case manually, because there are some weird overlayfs
// issues in docker where when the file does NOT exist, but we "open it" we get an
@ -342,12 +365,25 @@ impl KanidmClientBuilder {
ClientError::ConfigParseIssue(format!("{:?}", e))
})?;
let config: KanidmClientConfig = toml::from_str(&contents).map_err(|e| {
let mut config: KanidmClientConfig = toml::from_str(&contents).map_err(|e| {
error!("{:?}", e);
ClientError::ConfigParseIssue(format!("{:?}", e))
})?;
self.apply_config_options(config)
if let Some(instance_name) = instance {
if let Some(instance_config) = config.instances.remove(instance_name) {
self.apply_config_options(instance_config)
} else {
let emsg = format!(
"instance {} does not exist in config file {:?}",
instance_name, config_path
);
error!(%emsg);
Err(ClientError::ConfigParseIssue(emsg))
}
} else {
self.apply_config_options(config.default)
}
}
pub fn address(self, address: String) -> Self {
@ -1474,6 +1510,11 @@ impl KanidmClient {
self.perform_get_request(V1_AUTH_VALID).await
}
pub async fn get_public_jwk(&self, key_id: &str) -> Result<Jwk, ClientError> {
self.perform_get_request(&format!("/v1/jwk/{}", key_id))
.await
}
pub async fn whoami(&self) -> Result<Option<Entry>, ClientError> {
let response = self.client.get(self.make_url("/v1/self"));
@ -1938,9 +1979,12 @@ impl KanidmClient {
.await
}
pub async fn idm_domain_reset_token_key(&self) -> Result<(), ClientError> {
self.perform_delete_request("/v1/domain/_attr/es256_private_key_der")
.await
pub async fn idm_domain_revoke_key(&self, key_id: &str) -> Result<(), ClientError> {
self.perform_put_request(
"/v1/domain/_attr/key_action_revoke",
vec![key_id.to_string()],
)
.await
}
// ==== schema

View file

@ -108,6 +108,11 @@ pub const ATTR_INDEX: &str = "index";
pub const ATTR_IPANTHASH: &str = "ipanthash";
pub const ATTR_IPASSHPUBKEY: &str = "ipasshpubkey";
pub const ATTR_JWS_ES256_PRIVATE_KEY: &str = "jws_es256_private_key";
pub const ATTR_KEY_ACTION_ROTATE: &str = "key_action_rotate";
pub const ATTR_KEY_ACTION_REVOKE: &str = "key_action_revoke";
pub const ATTR_KEY_ACTION_IMPORT_JWS_ES256: &str = "key_action_import_jws_es256";
pub const ATTR_KEY_INTERNAL_DATA: &str = "key_internal_data";
pub const ATTR_KEY_PROVIDER: &str = "key_provider";
pub const ATTR_LAST_MODIFIED_CID: &str = "last_modified_cid";
pub const ATTR_LDAP_ALLOW_UNIX_PW_BIND: &str = "ldap_allow_unix_pw_bind";
pub const ATTR_LEGALNAME: &str = "legalname";
@ -233,3 +238,9 @@ pub const X_FORWARDED_FOR: &str = "x-forwarded-for";
/// Builtin object
pub const ENTRYCLASS_BUILTIN: &str = "builtin";
pub const ENTRYCLASS_KEY_PROVIDER: &str = "key_provider";
pub const ENTRYCLASS_KEY_PROVIDER_INTERNAL: &str = "key_provider_internal";
pub const ENTRYCLASS_KEY_OBJECT: &str = "key_object";
pub const ENTRYCLASS_KEY_OBJECT_JWT_ES256: &str = "key_object_jwt_es256";
pub const ENTRYCLASS_KEY_OBJECT_JWE_A128GCM: &str = "key_object_jwe_a128gcm";
pub const ENTRYCLASS_KEY_OBJECT_INTERNAL: &str = "key_object_internal";

View file

@ -56,6 +56,9 @@ pub enum ConsistencyError {
ChangeStateDesynchronised(u64),
RuvInconsistent(String),
DeniedName(Uuid),
KeyProviderUuidMissing { key_object: Uuid },
KeyProviderNoKeys { key_object: Uuid },
KeyProviderNotFound { key_object: Uuid, provider: Uuid },
}
#[derive(Serialize, Deserialize, Debug, ToSchema)]
@ -128,6 +131,7 @@ pub enum OperationError {
VS0001IncomingReplSshPublicKey,
// Value Errors
VL0001ValueSshPublicKeyString,
// SCIM
SC0001IncomingSshPublicKey,
// Migration
@ -136,6 +140,52 @@ pub enum OperationError {
MG0003ServerPhaseInvalidForMigration,
MG0004DomainLevelInDevelopment,
MG0005GidConstraintsNotMet,
//
KP0001KeyProviderNotLoaded,
KP0002KeyProviderInvalidClass,
KP0003KeyProviderInvalidType,
KP0004KeyProviderMissingAttributeName,
KP0005KeyProviderDuplicate,
KP0006KeyObjectJwtEs256Generation,
KP0007KeyProviderDefaultNotAvailable,
KP0008KeyObjectMissingUuid,
KP0009KeyObjectPrivateToDer,
KP0010KeyObjectSignerToVerifier,
KP0011KeyObjectMissingClass,
KP0012KeyObjectMissingProvider,
KP0012KeyProviderNotLoaded,
KP0013KeyObjectJwsEs256DerInvalid,
KP0014KeyObjectSignerToVerifier,
KP0015KeyObjectJwsEs256DerInvalid,
KP0016KeyObjectJwsEs256DerInvalid,
KP0017KeyProviderNoSuchKey,
KP0018KeyProviderNoSuchKey,
KP0019KeyProviderUnsupportedAlgorithm,
KP0020KeyObjectNoActiveSigningKeys,
KP0021KeyObjectJwsEs256Signature,
KP0022KeyObjectJwsNotAssociated,
KP0023KeyObjectJwsKeyRevoked,
KP0024KeyObjectJwsInvalid,
KP0025KeyProviderNotAvailable,
KP0026KeyObjectNoSuchKey,
KP0027KeyObjectPublicToDer,
KP0028KeyObjectImportJwsEs256DerInvalid,
KP0029KeyObjectSignerToVerifier,
KP0030KeyObjectPublicToDer,
KP0031KeyObjectNotFound,
KP0032KeyProviderNoSuchKey,
KP0033KeyProviderNoSuchKey,
KP0034KeyProviderUnsupportedAlgorithm,
KP0035KeyObjectJweA128GCMGeneration,
KP0036KeyObjectPrivateToBytes,
KP0037KeyObjectImportJweA128GCMInvalid,
KP0038KeyObjectImportJweA128GCMInvalid,
KP0039KeyObjectJweNotAssociated,
KP0040KeyObjectJweInvalid,
KP0041KeyObjectJweRevoked,
KP0042KeyObjectNoActiveEncryptionKeys,
KP0043KeyObjectJweA128GCMEncryption,
KP0044KeyObjectJwsPublicJwk,
// Plugins
PL0001GidOverlapsSystemRange,

View file

@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use serde_with::formats::SpaceSeparator;
use serde_with::{serde_as, skip_serializing_none, StringWithSeparator};
use url::Url;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
pub enum CodeChallengeMethod {
@ -124,7 +125,50 @@ impl From<GrantTypeReq> for AccessTokenRequest {
}
}
/// The
#[derive(Serialize, Debug, Clone, Deserialize)]
#[skip_serializing_none]
pub struct OAuth2RFC9068Token<V>
where
V: Clone,
{
/// The issuer of this token
pub iss: String,
/// Unique id of the subject
pub sub: Uuid,
/// client_id of the oauth2 rp
pub aud: String,
/// Expiry in UTC epoch seconds
pub exp: i64,
/// Not valid before.
pub nbf: i64,
/// Issued at time.
pub iat: i64,
/// -- NOT used, but part of the spec.
pub jti: Option<String>,
pub client_id: String,
#[serde(flatten)]
pub extensions: V,
}
/// Extensions for RFC 9068 Access Token
#[serde_as]
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OAuth2RFC9068TokenExtensions {
pub auth_time: Option<i64>,
pub acr: Option<String>,
pub amr: Option<Vec<String>>,
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
pub scope: BTreeSet<String>,
pub nonce: Option<String>,
pub session_id: Uuid,
pub parent_session_id: Option<Uuid>,
}
/// The response for an access token
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessTokenResponse {

View file

@ -2,6 +2,7 @@ use std::convert::TryFrom;
use std::fs;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use kanidm_proto::internal::{
ApiToken, AppLink, BackupCodesView, CURequest, CUSessionToken, CUStatus, CredentialStatus,
@ -20,6 +21,8 @@ use regex::Regex;
use tracing::{error, info, instrument, trace};
use uuid::Uuid;
use compact_jwt::{JweCompact, Jwk, JwsCompact};
use kanidmd_lib::be::BackendTransaction;
use kanidmd_lib::prelude::*;
use kanidmd_lib::{
@ -1096,11 +1099,16 @@ impl QueryServerReadV1 {
session_token: CUSessionToken,
eventid: Uuid,
) -> Result<CUStatus, OperationError> {
let session_token = JweCompact::from_str(&session_token.token)
.map(|token_enc| CredentialUpdateSessionToken { token_enc })
.map_err(|err| {
error!(?err, "malformed token");
OperationError::InvalidRequestState
})?;
// Don't proceed unless the token parses
let ct = duration_from_epoch_now();
let idms_cred_update = self.idms.cred_update_transaction().await;
let session_token = CredentialUpdateSessionToken {
token_enc: session_token.token,
};
idms_cred_update
.credential_update_status(&session_token, ct)
@ -1125,11 +1133,15 @@ impl QueryServerReadV1 {
scr: CURequest,
eventid: Uuid,
) -> Result<CUStatus, OperationError> {
let session_token = JweCompact::from_str(&session_token.token)
.map(|token_enc| CredentialUpdateSessionToken { token_enc })
.map_err(|err| {
error!(?err, "Invalid Token - Must be a compact JWE");
OperationError::InvalidRequestState
})?;
let ct = duration_from_epoch_now();
let idms_cred_update = self.idms.cred_update_transaction().await;
let session_token = CredentialUpdateSessionToken {
token_enc: session_token.token,
};
debug!(?scr);
@ -1380,14 +1392,14 @@ impl QueryServerReadV1 {
)]
pub async fn handle_oauth2_token_introspect(
&self,
client_authz: String,
client_auth_info: ClientAuthInfo,
intr_req: AccessTokenIntrospectRequest,
eventid: Uuid,
) -> Result<AccessTokenIntrospectResponse, Oauth2Error> {
let ct = duration_from_epoch_now();
let mut idms_prox_read = self.idms.proxy_read().await;
// Now we can send to the idm server for introspection checking.
idms_prox_read.check_oauth2_token_introspect(&client_authz, &intr_req, ct)
idms_prox_read.check_oauth2_token_introspect(&client_auth_info, &intr_req, ct)
}
#[instrument(
@ -1398,12 +1410,12 @@ impl QueryServerReadV1 {
pub async fn handle_oauth2_openid_userinfo(
&self,
client_id: String,
client_authz: String,
token: JwsCompact,
eventid: Uuid,
) -> Result<OidcToken, Oauth2Error> {
let ct = duration_from_epoch_now();
let mut idms_prox_read = self.idms.proxy_read().await;
idms_prox_read.oauth2_openid_userinfo(&client_id, &client_authz, ct)
idms_prox_read.oauth2_openid_userinfo(&client_id, token, ct)
}
#[instrument(
@ -1503,6 +1515,22 @@ impl QueryServerReadV1 {
})
}
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
)]
/// Retrieve a public jwk
pub async fn handle_public_jwk_get(
&self,
key_id: String,
eventid: Uuid,
) -> Result<Jwk, OperationError> {
let mut idms_prox_read = self.idms.proxy_read().await;
idms_prox_read.jws_public_jwk(key_id.as_str())
}
#[instrument(
level = "info",
skip_all,

View file

@ -47,6 +47,7 @@ impl QueryServerWriteV1 {
idms_prox_write
.scim_sync_generate_token(&gte, ct)
.map(|token| token.to_string())
.and_then(|r| idms_prox_write.commit().map(|_| r))
}

View file

@ -1,11 +1,13 @@
use std::iter;
use compact_jwt::JweCompact;
use kanidm_proto::internal::{
CUIntentToken, CUSessionToken, CUStatus, CreateRequest, DeleteRequest, ImageValue,
Modify as ProtoModify, ModifyList as ProtoModifyList, ModifyRequest,
Oauth2ClaimMapJoin as ProtoOauth2ClaimMapJoin, OperationError,
};
use kanidm_proto::v1::{AccountUnixExtend, Entry as ProtoEntry, GroupUnixExtend};
use std::str::FromStr;
use time::OffsetDateTime;
use tracing::{info, instrument, trace};
use uuid::Uuid;
@ -442,6 +444,7 @@ impl QueryServerWriteV1 {
idms_prox_write
.service_account_generate_api_token(&gte, ct)
.and_then(|r| idms_prox_write.commit().map(|_| r))
.map(|token| token.to_string())
}
#[instrument(
@ -609,7 +612,7 @@ impl QueryServerWriteV1 {
.map(|(tok, sta)| {
(
CUSessionToken {
token: tok.token_enc,
token: tok.token_enc.to_string(),
},
sta.into(),
)
@ -692,7 +695,7 @@ impl QueryServerWriteV1 {
.map(|(tok, sta)| {
(
CUSessionToken {
token: tok.token_enc,
token: tok.token_enc.to_string(),
},
sta.into(),
)
@ -709,11 +712,15 @@ impl QueryServerWriteV1 {
session_token: CUSessionToken,
eventid: Uuid,
) -> Result<(), OperationError> {
let session_token = JweCompact::from_str(session_token.token.as_str())
.map(|token_enc| CredentialUpdateSessionToken { token_enc })
.map_err(|err| {
error!(?err, "malformed token");
OperationError::InvalidRequestState
})?;
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write(ct).await;
let session_token = CredentialUpdateSessionToken {
token_enc: session_token.token,
};
idms_prox_write
.commit_credential_update(&session_token, ct)
@ -737,11 +744,15 @@ impl QueryServerWriteV1 {
session_token: CUSessionToken,
eventid: Uuid,
) -> Result<(), OperationError> {
let session_token = JweCompact::from_str(session_token.token.as_str())
.map(|token_enc| CredentialUpdateSessionToken { token_enc })
.map_err(|err| {
error!(?err, "malformed token");
OperationError::InvalidRequestState
})?;
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write(ct).await;
let session_token = CredentialUpdateSessionToken {
token_enc: session_token.token,
};
idms_prox_write
.cancel_credential_update(&session_token, ct)
@ -1671,15 +1682,14 @@ impl QueryServerWriteV1 {
)]
pub async fn handle_oauth2_token_exchange(
&self,
client_authz: Option<String>,
client_auth_info: ClientAuthInfo,
token_req: AccessTokenRequest,
eventid: Uuid,
) -> Result<AccessTokenResponse, Oauth2Error> {
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write(ct).await;
// Now we can send to the idm server for authorisation checking.
let resp =
idms_prox_write.check_oauth2_token_exchange(client_authz.as_deref(), &token_req, ct);
let resp = idms_prox_write.check_oauth2_token_exchange(&client_auth_info, &token_req, ct);
match &resp {
Err(Oauth2Error::InvalidGrant) | Ok(_) => {
@ -1698,14 +1708,14 @@ impl QueryServerWriteV1 {
)]
pub async fn handle_oauth2_token_revoke(
&self,
client_authz: String,
client_auth_info: ClientAuthInfo,
intr_req: TokenRevokeRequest,
eventid: Uuid,
) -> Result<(), Oauth2Error> {
let ct = duration_from_epoch_now();
let mut idms_prox_write = self.idms.proxy_write(ct).await;
idms_prox_write
.oauth2_token_revoke(&client_authz, &intr_req, ct)
.oauth2_token_revoke(&client_auth_info, &intr_req, ct)
.and_then(|()| idms_prox_write.commit().map_err(Oauth2Error::ServerError))
}
}

View file

@ -194,6 +194,7 @@ impl Modify for SecurityAddon {
super::v1_scim::sync_account_token_post,
super::v1_scim::sync_account_token_delete,
super::v1::debug_ipinfo,
super::v1::public_jwk_key_id_get,
),
components(
@ -277,7 +278,6 @@ impl Modify for SecurityAddon {
// terrible workaround for other things
response_schema::ScimEntry,
WebError,
)
),

View file

@ -11,6 +11,9 @@ use hyper::server::conn::AddrStream;
use kanidm_proto::constants::X_FORWARDED_FOR;
use kanidmd_lib::prelude::{ClientAuthInfo, ClientCertInfo, Source};
use compact_jwt::JwsCompact;
use std::str::FromStr;
use std::net::{IpAddr, SocketAddr};
use crate::https::ServerState;
@ -125,26 +128,41 @@ impl FromRequestParts<ServerState> for VerifiedClientInformation {
addr.ip()
};
let bearer_token = if let Some(header) = parts.headers.get(AUTHORISATION) {
header
let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) {
if let Some((authz_type, authz_data)) = header
.to_str()
.map_err(|err| {
warn!(?err, "Invalid bearer token, ignoring");
warn!(?err, "Invalid authz header, ignoring");
})
.ok()
.and_then(|s| s.split_once(' '))
.map(|(_, s)| s.to_string())
.or_else(|| {
warn!("bearer token format invalid, ignoring");
None
})
{
let authz_type = authz_type.to_lowercase();
if authz_type == "basic" {
(Some(authz_data.to_string()), None)
} else if authz_type == "bearer" {
if let Some(jwsc) = JwsCompact::from_str(authz_data).ok() {
(None, Some(jwsc))
} else {
warn!("bearer jws invalid");
(None, None)
}
} else {
warn!("authorisation header invalid, ignoring");
(None, None)
}
} else {
(None, None)
}
} else {
None
(None, None)
};
Ok(VerifiedClientInformation(ClientAuthInfo {
source: Source::Https(ip_addr),
bearer_token,
basic_authz,
client_cert,
}))
}

View file

@ -5,6 +5,7 @@ use axum::{
};
use kanidm_proto::constants::{KOPID, KVERSION};
use uuid::Uuid;
pub(crate) mod caching;
pub(crate) mod compression;
pub(crate) mod hsts_header;

View file

@ -4,10 +4,10 @@ use super::ServerState;
use crate::https::extractors::VerifiedClientInformation;
use axum::extract::{Path, Query, State};
use axum::http::header::{
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION, CONTENT_TYPE,
LOCATION, WWW_AUTHENTICATE,
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, LOCATION,
WWW_AUTHENTICATE,
};
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use axum::http::{HeaderValue, StatusCode};
use axum::middleware::from_fn;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
@ -478,30 +478,22 @@ async fn oauth2_authorise_reject(
}
#[axum_macros::debug_handler]
#[instrument(skip(state, kopid, headers), level = "DEBUG")]
#[instrument(skip(state, kopid, client_auth_info), level = "DEBUG")]
pub async fn oauth2_token_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
headers: HeaderMap,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Form(tok_req): Form<AccessTokenRequest>,
) -> impl IntoResponse {
// This is called directly by the resource server, where we then issue
// the token to the caller.
// Get the authz header (if present). Not all exchange types require this.
let client_authz = headers
.get("authorization")
.and_then(|hv| hv.to_str().ok())
.and_then(|h| h.split(' ').last())
.map(str::to_string);
// Do we change the method/path we take here based on the type of requested
// grant? Should we cease the delayed/async session update here and just opt
// for a wr txn?
match state
.qe_w_ref
.handle_oauth2_token_exchange(client_authz, tok_req, kopid.eventid)
.handle_oauth2_token_exchange(client_auth_info, tok_req, kopid.eventid)
.await
{
Ok(tok_res) => (
@ -609,39 +601,12 @@ pub async fn oauth2_token_introspect_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
headers: HeaderMap,
Form(intr_req): Form<AccessTokenIntrospectRequest>,
) -> impl IntoResponse {
let client_authz = match client_auth_info.bearer_token {
Some(val) => val,
None => {
error!("Bearer Authentication Not Provided, trying basic");
match headers.get(AUTHORIZATION) {
Some(val) => {
// LOL THIS IS HILARIOUSLY TERRIBLE BUT WE PARSE THE RAW OK
#[allow(clippy::unwrap_used)]
val.to_str()
.unwrap()
.strip_prefix("Basic ")
.unwrap()
.to_string()
}
None => {
#[allow(clippy::unwrap_used)]
return Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(Body::from("Invalid Bearer Authorisation"))
.unwrap();
}
}
}
};
request_trace!("Introspect Request - {:?}", intr_req);
let res = state
.qe_r_ref
.handle_oauth2_token_introspect(client_authz, intr_req, kopid.eventid)
.handle_oauth2_token_introspect(client_auth_info, intr_req, kopid.eventid)
.await;
match res {
@ -700,24 +665,11 @@ pub async fn oauth2_token_revoke_post(
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Form(intr_req): Form<TokenRevokeRequest>,
) -> impl IntoResponse {
// TODO: we should handle the session-based auth bit here I think maybe possibly there's no tests
let client_authz = match client_auth_info.bearer_token {
Some(val) => val,
None => {
return (
StatusCode::UNAUTHORIZED,
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
"",
)
.into_response();
}
};
request_trace!("Revoke Request - {:?}", intr_req);
let res = state
.qe_w_ref
.handle_oauth2_token_revoke(client_authz, intr_req, kopid.eventid)
.handle_oauth2_token_revoke(client_auth_info, intr_req, kopid.eventid)
.await;
match res {

View file

@ -6,7 +6,7 @@ use axum::middleware::from_fn;
use axum::response::{IntoResponse, Response};
use axum::routing::{delete, get, post, put};
use axum::{Extension, Json, Router};
use compact_jwt::{Jws, JwsSigner};
use compact_jwt::{Jwk, Jws, JwsSigner};
use kanidm_proto::constants::uri::V1_AUTH_VALID;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
@ -2834,7 +2834,7 @@ fn auth_session_state_management(
debug!("🧩 -> AuthState::Success");
match issue {
AuthIssueSession::Token => Ok(ProtoAuthState::Success(token)),
AuthIssueSession::Token => Ok(ProtoAuthState::Success(token.to_string())),
}
}
AuthState::Denied(reason) => {
@ -2908,8 +2908,37 @@ pub async fn debug_ipinfo(
Ok(Json::from(vec![ip_addr]))
}
#[utoipa::path(
get,
path = "/v1/jwk/{key_id}",
responses(
(status=200, body=Jwk, content_type="application/json"),
ApiResponseWithout200,
),
security(("token_jwt" = [])),
tag = "v1/jwk",
operation_id = "public_jwk_key_id_get"
)]
pub async fn public_jwk_key_id_get(
State(state): State<ServerState>,
Path(key_id): Path<String>,
Extension(kopid): Extension<KOpId>,
) -> Result<Json<Jwk>, WebError> {
if key_id.len() > 64 {
// Fast path to reject long KeyIDs
return Err(WebError::from(OperationError::NoMatchingEntries));
}
state
.qe_r_ref
.handle_public_jwk_get(key_id, kopid.eventid)
.await
.map(Json::from)
.map_err(WebError::from)
}
fn cacheable_routes(state: ServerState) -> Router<ServerState> {
Router::new()
.route("/v1/jwk/:key_id", get(public_jwk_key_id_get))
.route(
"/v1/person/:id/_radius/_token",
get(person_id_radius_token_get),

View file

@ -49,7 +49,7 @@ pub(crate) async fn oauth2_get(
tag = "v1/oauth2",
operation_id = "oauth2_basic_post"
)]
// TODO: what does this actually do? :D
/// Create a new Confidential OAuth2 client that authenticates with Http Basic.
pub(crate) async fn oauth2_basic_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
@ -76,7 +76,7 @@ pub(crate) async fn oauth2_basic_post(
tag = "v1/oauth2",
operation_id = "oauth2_public_post"
)]
// TODO: what does this actually do? :D
/// Create a new Public OAuth2 client
pub(crate) async fn oauth2_public_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,

View file

@ -28,6 +28,7 @@ harness = false
[dependencies]
base64 = { workspace = true }
base64urlsafedata = { workspace = true }
bitflags = { workspace = true }
compact_jwt = { workspace = true, features = ["openssl", "hsm-crypto"] }
concread = { workspace = true }
dyn-clone = { workspace = true }
@ -107,6 +108,8 @@ webauthn-authenticator-rs = { workspace = true }
futures = { workspace = true }
kanidmd_lib_macros = { workspace = true }
compact_jwt = { workspace = true, features = ["openssl", "hsm-crypto", "unsafe_release_without_verify"] }
[build-dependencies]
hashbrown = { workspace = true }
kanidm_build_profiles = { workspace = true }

View file

@ -18,7 +18,7 @@ use webauthn_rs_core::proto::{COSEKey, UserVerificationPolicy};
use crate::repl::cid::Cid;
pub use kanidm_lib_crypto::DbPasswordV1;
#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, Ord, PartialOrd, PartialEq, Eq, Clone)]
pub struct DbCidV1 {
#[serde(rename = "t")]
pub timestamp: Duration,
@ -595,6 +595,31 @@ pub enum DbValueImage {
},
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum DbValueKeyUsage {
JwsEs256,
JweA128GCM,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum DbValueKeyStatus {
Valid,
Retained,
Revoked,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum DbValueKeyInternal {
V1 {
id: String,
usage: DbValueKeyUsage,
valid_from: u64,
status: DbValueKeyStatus,
status_cid: DbCidV1,
der: Vec<u8>,
},
}
#[derive(Serialize, Deserialize, Debug)]
pub enum DbValueV1 {
#[serde(rename = "U8")]
@ -748,14 +773,19 @@ pub enum DbValueSetV2 {
CredentialType(Vec<u16>),
#[serde(rename = "WC")]
WebauthnAttestationCaList { ca_list: AttestationCaList },
#[serde(rename = "KI")]
KeyInternal(Vec<DbValueKeyInternal>),
#[serde(rename = "HS")]
HexString(Vec<String>),
}
impl DbValueSetV2 {
pub fn len(&self) -> usize {
match self {
DbValueSetV2::Utf8(set) => set.len(),
DbValueSetV2::Iutf8(set) => set.len(),
DbValueSetV2::Iname(set) => set.len(),
DbValueSetV2::Utf8(set)
| DbValueSetV2::Iutf8(set)
| DbValueSetV2::HexString(set)
| DbValueSetV2::Iname(set) => set.len(),
DbValueSetV2::Uuid(set) => set.len(),
DbValueSetV2::Bool(set) => set.len(),
DbValueSetV2::SyntaxType(set) => set.len(),
@ -797,6 +827,7 @@ impl DbValueSetV2 {
// represents the bytes of SINGLE(!) key
DbValueSetV2::CredentialType(set) => set.len(),
DbValueSetV2::WebauthnAttestationCaList { ca_list } => ca_list.len(),
DbValueSetV2::KeyInternal(set) => set.len(),
}
}

View file

@ -1539,7 +1539,10 @@ impl<'a> BackendWriteTransaction<'a> {
idl.insert_id(e_id);
if cfg!(debug_assertions)
&& attr == Attribute::Uuid.as_ref() && itype == IndexType::Equality {
trace!("{:?}", idl);
// This means a duplicate UUID has appeared in the index.
if idl.len() > 1 {
trace!(duplicate_idl = ?idl, ?idx_key);
}
debug_assert!(idl.len() <= 1);
}
self.idlayer.write_idl(attr, itype, &idx_key, &idl)
@ -1559,7 +1562,10 @@ impl<'a> BackendWriteTransaction<'a> {
Some(mut idl) => {
idl.remove_id(e_id);
if cfg!(debug_assertions) && attr == Attribute::Uuid.as_ref() && itype == IndexType::Equality {
trace!("{:?}", idl);
// This means a duplicate UUID has appeared in the index.
if idl.len() > 1 {
trace!(duplicate_idl = ?idl, ?idx_key);
}
debug_assert!(idl.len() <= 1);
}
self.idlayer.write_idl(attr, itype, &idx_key, &idl)

View file

@ -934,6 +934,61 @@ lazy_static! {
};
}
lazy_static! {
pub static ref IDM_ACP_DOMAIN_ADMIN_DL6: BuiltinAcp = BuiltinAcp {
classes: vec![
EntryClass::Object,
EntryClass::AccessControlProfile,
EntryClass::AccessControlModify,
EntryClass::AccessControlSearch
],
name: "idm_acp_domain_admin",
uuid: UUID_IDM_ACP_DOMAIN_ADMIN_V1,
description: "Builtin IDM Control for granting domain info administration locally",
receiver: BuiltinAcpReceiver::Group(vec![UUID_DOMAIN_ADMINS]),
target: BuiltinAcpTarget::Filter(ProtoFilter::And(vec![
ProtoFilter::Eq(
Attribute::Uuid.to_string(),
STR_UUID_DOMAIN_INFO.to_string()
),
FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
])),
search_attrs: vec![
Attribute::Class,
Attribute::Name,
Attribute::Uuid,
Attribute::DomainDisplayName,
Attribute::DomainName,
Attribute::DomainLdapBasedn,
Attribute::DomainSsid,
Attribute::DomainUuid,
// Grants read access to the key object.
// But this means we have to specify every type of key object?
// Future william problem ...
Attribute::KeyInternalData,
Attribute::LdapAllowUnixPwBind,
Attribute::Version,
],
modify_removed_attrs: vec![
Attribute::DomainDisplayName,
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapAllowUnixPwBind,
Attribute::KeyActionRevoke,
Attribute::KeyActionRotate,
],
modify_present_attrs: vec![
Attribute::DomainDisplayName,
Attribute::DomainLdapBasedn,
Attribute::DomainSsid,
Attribute::LdapAllowUnixPwBind,
Attribute::KeyActionRevoke,
Attribute::KeyActionRotate,
],
..Default::default()
};
}
lazy_static! {
pub static ref IDM_ACP_SYNC_ACCOUNT_MANAGE_V1: BuiltinAcp = BuiltinAcp {
classes: vec![

View file

@ -100,6 +100,11 @@ pub enum Attribute {
IpaNtHash,
IpaSshPubKey,
JwsEs256PrivateKey,
KeyActionRotate,
KeyActionRevoke,
KeyActionImportJwsEs256,
KeyInternalData,
KeyProvider,
LastModifiedCid,
LdapAllowUnixPwBind,
/// An LDAP Compatible emailAddress
@ -290,6 +295,11 @@ impl TryFrom<String> for Attribute {
ATTR_IPANTHASH => Attribute::IpaNtHash,
ATTR_IPASSHPUBKEY => Attribute::IpaSshPubKey,
ATTR_JWS_ES256_PRIVATE_KEY => Attribute::JwsEs256PrivateKey,
ATTR_KEY_ACTION_ROTATE => Attribute::KeyActionRotate,
ATTR_KEY_ACTION_REVOKE => Attribute::KeyActionRevoke,
ATTR_KEY_ACTION_IMPORT_JWS_ES256 => Attribute::KeyActionImportJwsEs256,
ATTR_KEY_INTERNAL_DATA => Attribute::KeyInternalData,
ATTR_KEY_PROVIDER => Attribute::KeyProvider,
ATTR_LAST_MODIFIED_CID => Attribute::LastModifiedCid,
ATTR_LDAP_ALLOW_UNIX_PW_BIND => Attribute::LdapAllowUnixPwBind,
ATTR_LDAP_EMAIL_ADDRESS => Attribute::LdapEmailAddress,
@ -456,6 +466,11 @@ impl From<Attribute> for &'static str {
Attribute::IpaNtHash => ATTR_IPANTHASH,
Attribute::IpaSshPubKey => ATTR_IPASSHPUBKEY,
Attribute::JwsEs256PrivateKey => ATTR_JWS_ES256_PRIVATE_KEY,
Attribute::KeyActionRotate => ATTR_KEY_ACTION_ROTATE,
Attribute::KeyActionRevoke => ATTR_KEY_ACTION_REVOKE,
Attribute::KeyActionImportJwsEs256 => ATTR_KEY_ACTION_IMPORT_JWS_ES256,
Attribute::KeyInternalData => ATTR_KEY_INTERNAL_DATA,
Attribute::KeyProvider => ATTR_KEY_PROVIDER,
Attribute::LastModifiedCid => ATTR_LAST_MODIFIED_CID,
Attribute::LdapAllowUnixPwBind => ATTR_LDAP_ALLOW_UNIX_PW_BIND,
Attribute::LdapEmailAddress => ATTR_LDAP_EMAIL_ADDRESS,
@ -608,6 +623,12 @@ pub enum EntryClass {
DynGroup,
ExtensibleObject,
Group,
KeyProvider,
KeyProviderInternal,
KeyObject,
KeyObjectJwtEs256,
KeyObjectJweA128GCM,
KeyObjectInternal,
MemberOf,
OAuth2ResourceServer,
OAuth2ResourceServerBasic,
@ -655,6 +676,12 @@ impl From<EntryClass> for &'static str {
EntryClass::DynGroup => ATTR_DYNGROUP,
EntryClass::ExtensibleObject => "extensibleobject",
EntryClass::Group => ATTR_GROUP,
EntryClass::KeyProvider => ENTRYCLASS_KEY_PROVIDER,
EntryClass::KeyProviderInternal => ENTRYCLASS_KEY_PROVIDER_INTERNAL,
EntryClass::KeyObject => ENTRYCLASS_KEY_OBJECT,
EntryClass::KeyObjectJwtEs256 => ENTRYCLASS_KEY_OBJECT_JWT_ES256,
EntryClass::KeyObjectJweA128GCM => ENTRYCLASS_KEY_OBJECT_JWE_A128GCM,
EntryClass::KeyObjectInternal => ENTRYCLASS_KEY_OBJECT_INTERNAL,
EntryClass::MemberOf => "memberof",
EntryClass::OAuth2ResourceServer => "oauth2_resource_server",
EntryClass::OAuth2ResourceServerBasic => "oauth2_resource_server_basic",
@ -758,7 +785,7 @@ lazy_static! {
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Uuid, Value::Uuid(UUID_SYSTEM_INFO)),
(
Attribute::Description,
Attribute::Description,
Value::new_utf8s("System (local) info and metadata object.")
),
(Attribute::Version, Value::Uint32(19))
@ -771,7 +798,22 @@ Attribute::Description,
(Attribute::Name, Value::new_iname("domain_local")),
(Attribute::Uuid, Value::Uuid(UUID_DOMAIN_INFO)),
(
Attribute::Description,
Attribute::Description,
Value::new_utf8s("This local domain's info and metadata object.")
)
);
pub static ref E_DOMAIN_INFO_DL6: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::DomainInfo.to_value()),
(Attribute::Class, EntryClass::System.to_value()),
(Attribute::Class, EntryClass::KeyObject.to_value()),
(Attribute::Class, EntryClass::KeyObjectJwtEs256.to_value()),
(Attribute::Class, EntryClass::KeyObjectJweA128GCM.to_value()),
(Attribute::Name, Value::new_iname("domain_local")),
(Attribute::Uuid, Value::Uuid(UUID_DOMAIN_INFO)),
(
Attribute::Description,
Value::new_utf8s("This local domain's info and metadata object.")
)
);

View file

@ -0,0 +1,18 @@
use crate::constants::entries::{Attribute, EntryClass};
use crate::constants::uuids::UUID_KEY_PROVIDER_INTERNAL;
use crate::entry::{Entry, EntryInit, EntryInitNew, EntryNew};
use crate::value::Value;
lazy_static! {
pub static ref E_KEY_PROVIDER_INTERNAL_DL6: EntryInitNew = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::KeyProvider.to_value()),
(Attribute::Class, EntryClass::KeyProviderInternal.to_value()),
(Attribute::Uuid, Value::Uuid(UUID_KEY_PROVIDER_INTERNAL)),
(Attribute::Name, Value::new_iname("key_provider_internal")),
(
Attribute::Description,
Value::new_utf8s("The default database internal cryptographic key provider.")
)
);
}

View file

@ -3,18 +3,20 @@
pub mod acp;
pub mod entries;
pub mod groups;
mod key_providers;
pub mod schema;
pub mod system_config;
pub mod uuids;
pub mod values;
pub use crate::constants::acp::*;
pub use crate::constants::entries::*;
pub use crate::constants::groups::*;
pub use crate::constants::schema::*;
pub use crate::constants::system_config::*;
pub use crate::constants::uuids::*;
pub use crate::constants::values::*;
pub use self::acp::*;
pub use self::entries::*;
pub use self::groups::*;
pub use self::key_providers::*;
pub use self::schema::*;
pub use self::system_config::*;
pub use self::uuids::*;
pub use self::values::*;
use std::time::Duration;

View file

@ -443,6 +443,7 @@ pub static ref SCHEMA_ATTR_JWS_ES256_PRIVATE_KEY: SchemaAttribute = SchemaAttrib
..Default::default()
};
// TO BE REMOVED IN A FUTURE RELEASE
pub static ref SCHEMA_ATTR_PRIVATE_COOKIE_KEY: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_PRIVATE_COOKIE_KEY,
name: Attribute::PrivateCookieKey.into(),
@ -638,6 +639,57 @@ pub static ref SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6: SchemaAttribute = S
..Default::default()
};
pub static ref SCHEMA_ATTR_KEY_INTERNAL_DATA_DL6: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_KEY_INTERNAL_DATA,
name: Attribute::KeyInternalData.into(),
description: "".to_string(),
multivalue: true,
syntax: SyntaxType::KeyInternal,
..Default::default()
};
pub static ref SCHEMA_ATTR_KEY_PROVIDER_DL6: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_KEY_PROVIDER,
name: Attribute::KeyProvider.into(),
description: "".to_string(),
multivalue: false,
syntax: SyntaxType::ReferenceUuid,
..Default::default()
};
pub static ref SCHEMA_ATTR_KEY_ACTION_ROTATE_DL6: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_KEY_ACTION_ROTATE,
name: Attribute::KeyActionRotate.into(),
description: "".to_string(),
multivalue: false,
// Ephemeral action.
phantom: true,
syntax: SyntaxType::DateTime,
..Default::default()
};
pub static ref SCHEMA_ATTR_KEY_ACTION_REVOKE_DL6: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_KEY_ACTION_REVOKE,
name: Attribute::KeyActionRevoke.into(),
description: "".to_string(),
multivalue: true,
// Ephemeral action.
phantom: true,
syntax: SyntaxType::HexString,
..Default::default()
};
pub static ref SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256_DL6: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256,
name: Attribute::KeyActionImportJwsEs256.into(),
description: "".to_string(),
multivalue: true,
// Ephemeral action.
phantom: true,
syntax: SyntaxType::PrivateBinary,
..Default::default()
};
// === classes ===
pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
@ -787,7 +839,7 @@ pub static ref SCHEMA_CLASS_ACCOUNT: SchemaClass = SchemaClass {
Attribute::DisplayName.into(),
Attribute::Name.into(),
Attribute::Spn.into()
],
],
systemsupplements: vec![
EntryClass::Person.into(),
EntryClass::ServiceAccount.into(),
@ -871,7 +923,29 @@ pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT_DL6: SchemaClass = SchemaClass {
Attribute::Mail.into(),
Attribute::PrimaryCredential.into(),
Attribute::ApiTokenSession.into(),
Attribute::JwsEs256PrivateKey.into(),
],
systemexcludes: vec![EntryClass::Person.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_SERVICE_ACCOUNT_DL7: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SERVICE_ACCOUNT,
name: EntryClass::ServiceAccount.into(),
description: "Object representation of service account".to_string(),
sync_allowed: true,
systemmay: vec![
Attribute::SshPublicKey.into(),
Attribute::UserAuthTokenSession.into(),
Attribute::OAuth2Session.into(),
Attribute::OAuth2ConsentScopeMap.into(),
Attribute::Description.into(),
Attribute::Mail.into(),
Attribute::PrimaryCredential.into(),
Attribute::ApiTokenSession.into(),
],
systemexcludes: vec![EntryClass::Person.into()],
@ -894,11 +968,39 @@ pub static ref SCHEMA_CLASS_SYNC_ACCOUNT: SchemaClass = SchemaClass {
..Default::default()
};
// domain_info type
// domain_uuid
// domain_name <- should be the dns name?
// domain_ssid <- for radius
//
pub static ref SCHEMA_CLASS_SYNC_ACCOUNT_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT,
name: EntryClass::SyncAccount.into(),
description: "Object representation of sync account".to_string(),
systemmust: vec![Attribute::Name.into()],
systemmay: vec![
Attribute::SyncTokenSession.into(),
Attribute::SyncCookie.into(),
Attribute::SyncCredentialPortal.into(),
Attribute::SyncYieldAuthority.into(),
Attribute::JwsEs256PrivateKey.into(),
],
systemexcludes: vec![EntryClass::Account.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_SYNC_ACCOUNT_DL7: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_SYNC_ACCOUNT,
name: EntryClass::SyncAccount.into(),
description: "Object representation of sync account".to_string(),
systemmust: vec![Attribute::Name.into()],
systemmay: vec![
Attribute::SyncTokenSession.into(),
Attribute::SyncCookie.into(),
Attribute::SyncCredentialPortal.into(),
Attribute::SyncYieldAuthority.into(),
],
systemexcludes: vec![EntryClass::Account.into()],
..Default::default()
};
pub static ref SCHEMA_CLASS_DOMAIN_INFO: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
name: EntryClass::DomainInfo.into(),
@ -922,6 +1024,49 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO: SchemaClass = SchemaClass {
..Default::default()
};
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
name: EntryClass::DomainInfo.into(),
description: "Local domain information and configuration".to_string(),
systemmay: vec![
Attribute::DomainSsid.into(),
Attribute::DomainLdapBasedn.into(),
Attribute::LdapAllowUnixPwBind.into(),
Attribute::PrivateCookieKey.into(),
Attribute::FernetPrivateKeyStr.into(),
Attribute::Es256PrivateKeyDer.into(),
],
systemmust: vec![
Attribute::Name.into(),
Attribute::DomainUuid.into(),
Attribute::DomainName.into(),
Attribute::DomainDisplayName.into(),
Attribute::Version.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL7: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_DOMAIN_INFO,
name: EntryClass::DomainInfo.into(),
description: "Local domain information and configuration".to_string(),
systemmay: vec![
Attribute::DomainSsid.into(),
Attribute::DomainLdapBasedn.into(),
Attribute::LdapAllowUnixPwBind.into(),
],
systemmust: vec![
Attribute::Name.into(),
Attribute::DomainUuid.into(),
Attribute::DomainName.into(),
Attribute::DomainDisplayName.into(),
Attribute::Version.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_POSIXGROUP: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_POSIXGROUP,
name: EntryClass::PosixGroup.into(),
@ -1080,4 +1225,76 @@ pub static ref SCHEMA_CLASS_OAUTH2_RS_PUBLIC_DL4: SchemaClass = SchemaClass {
..Default::default()
};
// =========================================
// KeyProviders
pub static ref SCHEMA_CLASS_KEY_PROVIDER_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_KEY_PROVIDER,
name: EntryClass::KeyProvider.into(),
description: "A provider for cryptographic key storage and operations".to_string(),
systemmay: vec![
Attribute::Description.into(),
],
systemmust: vec![
Attribute::Name.into(),
],
systemsupplements: vec![
EntryClass::KeyProviderInternal.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_KEY_PROVIDER_INTERNAL_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_KEY_PROVIDER_INTERNAL,
name: EntryClass::KeyProviderInternal.into(),
description: "The Kanidm internal cryptographic key provider".to_string(),
..Default::default()
};
// =========================================
// KeyObjects
pub static ref SCHEMA_CLASS_KEY_OBJECT_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_KEY_OBJECT,
name: EntryClass::KeyObject.into(),
description: "A cryptographic key object that can be used by a provider".to_string(),
systemmust: vec![
Attribute::KeyProvider.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_KEY_OBJECT_JWT_ES256,
name: EntryClass::KeyObjectJwtEs256.into(),
description: "A marker class indicating that this keyobject must provide jwt es256 capability.".to_string(),
systemsupplements: vec![
EntryClass::KeyObject.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM,
name: EntryClass::KeyObjectJweA128GCM.into(),
description: "A marker class indicating that this keyobject must provide jwe aes-256-gcm capability.".to_string(),
systemsupplements: vec![
EntryClass::KeyObject.into(),
],
..Default::default()
};
pub static ref SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6: SchemaClass = SchemaClass {
uuid: UUID_SCHEMA_CLASS_KEY_OBJECT_INTERNAL,
name: EntryClass::KeyObjectInternal.into(),
description: "A cryptographic key object that can be used by the internal provider".to_string(),
systemmay: vec![
Attribute::KeyInternalData.into(),
],
systemsupplements: vec![
EntryClass::KeyObject.into(),
],
..Default::default()
};
);

View file

@ -282,6 +282,25 @@ pub const UUID_SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000162");
pub const UUID_SCHEMA_CLASS_BUILTIN: Uuid = uuid!("00000000-0000-0000-0000-ffff00000163");
pub const UUID_SCHEMA_CLASS_KEY_PROVIDER: Uuid = uuid!("00000000-0000-0000-0000-ffff00000164");
pub const UUID_SCHEMA_CLASS_KEY_PROVIDER_INTERNAL: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000165");
pub const UUID_SCHEMA_CLASS_KEY_OBJECT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000166");
pub const UUID_SCHEMA_CLASS_KEY_OBJECT_INTERNAL: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000167");
pub const UUID_SCHEMA_CLASS_KEY_OBJECT_JWT_ES256: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000168");
pub const UUID_SCHEMA_ATTR_KEY_INTERNAL_DATA: Uuid = uuid!("00000000-0000-0000-0000-ffff00000169");
pub const UUID_SCHEMA_ATTR_KEY_PROVIDER: Uuid = uuid!("00000000-0000-0000-0000-ffff00000170");
pub const UUID_SCHEMA_ATTR_KEY_ACTION_REVOKE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000171");
pub const UUID_SCHEMA_ATTR_KEY_ACTION_ROTATE: Uuid = uuid!("00000000-0000-0000-0000-ffff00000172");
pub const UUID_SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000173");
pub const UUID_SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000174");
// System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
pub const UUID_SYSTEM_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000001");
@ -394,6 +413,8 @@ pub const UUID_IDM_ACP_HP_GROUP_UNIX_MANAGE_V1: Uuid =
pub const UUID_IDM_ACP_GROUP_UNIX_MANAGE_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000068");
pub const UUID_IDM_ACP_ACCOUNT_UNIX_EXTEND_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000069");
pub const UUID_KEY_PROVIDER_INTERNAL: Uuid = uuid!("00000000-0000-0000-0000-ffffff000070");
// End of system ranges
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");
pub const UUID_ANONYMOUS: Uuid = uuid!("00000000-0000-0000-0000-ffffffffffff");

View file

@ -1099,7 +1099,6 @@ impl Entry<EntryIncremental, EntryCommitted> {
}
impl<STATE> Entry<EntryInvalid, STATE> {
// This is only used in tests today, but I don't want to cfg test it.
pub(crate) fn get_uuid(&self) -> Option<Uuid> {
self.attrs
.get(Attribute::Uuid.as_ref())
@ -3303,6 +3302,18 @@ where
}
}
/// Merge the content from the new ValueSet into the existing ValueSet. If no existing
/// ValueSet is present, then these data are inserted.
pub fn merge_ava_set(&mut self, attr: Attribute, vs: ValueSet) -> Result<(), OperationError> {
self.valid.ecstate.change_ava(&self.valid.cid, attr);
if let Some(existing_vs) = self.attrs.get_mut(attr.as_ref()) {
existing_vs.merge(&vs)
} else {
self.attrs.insert(attr.into(), vs);
Ok(())
}
}
/// Apply the content of this modlist to this entry, enforcing the expressed state.
pub fn apply_modlist(
&mut self,

View file

@ -5,9 +5,10 @@
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use compact_jwt::{Jws, JwsEs256Signer, JwsSigner};
use compact_jwt::Jws;
use hashbrown::HashSet;
use kanidm_proto::internal::UserAuthToken;
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
@ -29,6 +30,7 @@ use crate::idm::delayed::{
};
use crate::idm::AuthState;
use crate::prelude::*;
use crate::server::keys::KeyObject;
use crate::value::{Session, SessionState};
use time::OffsetDateTime;
@ -916,13 +918,20 @@ pub(crate) struct AuthSession {
// Where did the event come from?
source: Source,
// The cryptographic provider to encrypt or sign anything in this operation.
key_object: Arc<KeyObject>,
}
impl AuthSession {
/// Create a new auth session, based on the available credential handlers of the account.
/// the session is a whole encapsulated unit of what we need to proceed, so that subsequent
/// or interleved write operations do not cause inconsistency in this process.
pub fn new(asd: AuthSessionData<'_>, privileged: bool) -> (Option<Self>, AuthState) {
pub fn new(
asd: AuthSessionData<'_>,
privileged: bool,
key_object: Arc<KeyObject>,
) -> (Option<Self>, AuthState) {
// During this setup, determine the credential handler that we'll be using
// for this session. This is currently based on presentation of an application
// id.
@ -1008,6 +1017,7 @@ impl AuthSession {
issue: asd.issue,
intent: AuthIntent::InitialAuth { privileged },
source: asd.client_auth_info.source,
key_object,
};
// Get the set of mechanisms that can proceed. This is tied
// to the session so that it can mutate state and have progression
@ -1029,6 +1039,7 @@ impl AuthSession {
session_id: Uuid,
session: &Session,
cred_id: Uuid,
key_object: Arc<KeyObject>,
) -> (Option<Self>, AuthState) {
/// An inner enum to allow us to more easily define state within this fn
enum State {
@ -1149,6 +1160,7 @@ impl AuthSession {
session_expiry,
},
source: asd.client_auth_info.source,
key_object,
};
let as_state = AuthState::Continue(allow);
@ -1254,7 +1266,6 @@ impl AuthSession {
async_tx: &Sender<DelayedAction>,
audit_tx: &Sender<AuditEvent>,
webauthn: &Webauthn,
uat_jwt_signer: &JwsEs256Signer,
pw_badlist: &HashSet<String>,
) -> Result<AuthState, OperationError> {
let (next_state, response) = match &mut self.state {
@ -1282,16 +1293,10 @@ impl AuthSession {
})?;
// Now encrypt and prepare the token for return to the client.
let token = uat_jwt_signer
// Do we want to embed this? Or just give the URL? I think we embed
// as we only need the client to be able to check it's not tampered, but
// this isn't a root of trust.
.sign(&jwt)
.map(|jwts| jwts.to_string())
.map_err(|e| {
admin_error!(?e, "Failed to sign UserAuthToken to Jwt");
OperationError::InvalidState
})?;
let token = self.key_object.jws_es256_sign(&jwt, time).map_err(|e| {
admin_error!(?e, "Failed to sign UserAuthToken to Jwt");
OperationError::InvalidState
})?;
(
Some(AuthSessionState::Success),
@ -1476,10 +1481,9 @@ impl AuthSession {
#[cfg(test)]
mod tests {
use std::str::FromStr;
use std::time::Duration;
use compact_jwt::{JwsCompact, JwsEs256Signer, JwsEs256Verifier, JwsVerifier};
use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
use hashbrown::HashSet;
use kanidm_proto::internal::{UatPurpose, UserAuthToken};
use kanidm_proto::v1::{AuthAllowed, AuthCredential, AuthIssueSession, AuthMech};
@ -1499,6 +1503,7 @@ mod tests {
use crate::idm::delayed::DelayedAction;
use crate::idm::AuthState;
use crate::prelude::*;
use crate::server::keys::KeyObjectInternal;
use crate::utils::readable_password_from_random;
use kanidm_lib_crypto::CryptoPolicy;
@ -1517,12 +1522,6 @@ mod tests {
.unwrap()
}
fn create_jwt_signer() -> JwsEs256Signer {
JwsEs256Signer::generate_es256()
.expect("failed to construct signer.")
.set_sign_option_embed_jwk(true)
}
#[test]
fn test_idm_authsession_anonymous_auth_mech() {
sketching::test_init();
@ -1539,7 +1538,9 @@ mod tests {
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let (session, state) = AuthSession::new(asd, false);
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, false, key_object);
if let AuthState::Choose(auth_mechs) = state {
assert!(auth_mechs.iter().any(|x| matches!(x, AuthMech::Anonymous)));
} else {
@ -1575,7 +1576,8 @@ mod tests {
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let (session, state) = AuthSession::new(asd, $privileged);
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, $privileged, key_object);
let mut session = session.unwrap();
if let AuthState::Choose(auth_mechs) = state {
@ -1617,14 +1619,12 @@ mod tests {
start_password_session!(&mut audit, account, &webauthn, false);
let attempt = AuthCredential::Password("bad_password".to_string());
let jws_signer = create_jwt_signer();
match session.validate_creds(
&attempt,
Duration::from_secs(0),
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(_)) => {}
@ -1648,14 +1648,10 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(jwt, AuthIssueSession::Token)) => {
let jwsc = JwsCompact::from_str(&jwt).expect("Failed to parse jwt");
let jws_verifier =
JwsEs256Verifier::try_from(jwsc.get_jwk_pubkey().unwrap()).unwrap();
Ok(AuthState::Success(jwsc, AuthIssueSession::Token)) => {
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
jws_verifier
.verify(&jwsc)
@ -1708,7 +1704,6 @@ mod tests {
#[test]
fn test_idm_authsession_simple_password_badlist() {
sketching::test_init();
let jws_signer = create_jwt_signer();
let webauthn = create_webauthn();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
@ -1731,7 +1726,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG),
@ -1762,7 +1756,8 @@ mod tests {
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let (session, state) = AuthSession::new(asd, false);
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, false, key_object);
let mut session = session.expect("Session was unable to be created.");
if let AuthState::Choose(auth_mechs) = state {
@ -1813,7 +1808,6 @@ mod tests {
fn test_idm_authsession_totp_password_mech() {
sketching::test_init();
let webauthn = create_webauthn();
let jws_signer = create_jwt_signer();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
@ -1857,7 +1851,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
@ -1883,7 +1876,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
@ -1906,7 +1898,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG),
@ -1931,7 +1922,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -1943,7 +1933,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG),
@ -1968,7 +1957,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -1980,7 +1968,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
@ -2003,7 +1990,6 @@ mod tests {
fn test_idm_authsession_password_mfa_badlist() {
sketching::test_init();
let webauthn = create_webauthn();
let jws_signer = create_jwt_signer();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
@ -2045,7 +2031,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2057,7 +2042,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == PW_BADLIST_MSG),
@ -2090,7 +2074,8 @@ mod tests {
ct: duration_from_epoch_now(),
client_auth_info: Source::Internal.into(),
};
let (session, state) = AuthSession::new(asd, false);
let key_object = KeyObjectInternal::new_test();
let (session, state) = AuthSession::new(asd, false, key_object);
let mut session = session.unwrap();
if let AuthState::Choose(auth_mechs) = state {
@ -2186,7 +2171,6 @@ mod tests {
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_passkey(account.name.as_str());
let jws_signer = create_jwt_signer();
// Now create the credential for the account.
account.passkeys = btreemap![(Uuid::new_v4(), ("soft".to_string(), wan_cred))];
@ -2204,7 +2188,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&Default::default(),
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
@ -2232,7 +2215,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&Default::default(),
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
@ -2267,7 +2249,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&Default::default(),
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG),
@ -2316,7 +2297,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&Default::default(),
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG),
@ -2345,7 +2325,6 @@ mod tests {
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
let jws_signer = create_jwt_signer();
let pw_good = "test_password";
let pw_bad = "bad_password";
@ -2369,7 +2348,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
@ -2393,7 +2371,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
@ -2428,7 +2405,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG),
@ -2458,7 +2434,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2470,7 +2445,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG),
@ -2506,7 +2480,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2518,7 +2491,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
@ -2552,7 +2524,6 @@ mod tests {
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
let (webauthn, mut wa, wan_cred) = setup_webauthn_securitykey(account.name.as_str());
let jws_signer = create_jwt_signer();
let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
let totp_good = totp
@ -2587,7 +2558,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
@ -2611,7 +2581,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_TOTP_MSG),
@ -2644,7 +2613,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_WEBAUTHN_MSG),
@ -2674,7 +2642,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2686,7 +2653,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG),
@ -2716,7 +2682,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2728,7 +2693,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG),
@ -2752,7 +2716,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2764,7 +2727,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
@ -2794,7 +2756,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2806,7 +2767,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
@ -2833,7 +2793,6 @@ mod tests {
#[test]
fn test_idm_authsession_backup_code_mech() {
sketching::test_init();
let jws_signer = create_jwt_signer();
let webauthn = create_webauthn();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
@ -2886,7 +2845,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_AUTH_TYPE_MSG),
@ -2909,7 +2867,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_BACKUPCODE_MSG),
@ -2933,7 +2890,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2945,7 +2901,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Denied(msg)) => assert!(msg == BAD_PASSWORD_MSG),
@ -2975,7 +2930,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -2987,7 +2941,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
@ -3019,7 +2972,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -3031,7 +2983,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
@ -3057,7 +3008,6 @@ mod tests {
// checks handling when multiple TOTP's are registered.
sketching::test_init();
let webauthn = create_webauthn();
let jws_signer = create_jwt_signer();
// create the ent
let mut account: Account = BUILTIN_ACCOUNT_ADMIN.clone().into();
@ -3102,7 +3052,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -3114,7 +3063,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
@ -3138,7 +3086,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
@ -3150,7 +3097,6 @@ mod tests {
&async_tx,
&audit_tx,
&webauthn,
&jws_signer,
&pw_badlist_cache,
) {
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}

View file

@ -26,6 +26,8 @@ use crate::prelude::*;
use crate::server::access::Access;
use crate::utils::{backup_code_from_random, readable_password_from_random, uuid_from_duration};
use crate::value::{CredUpdateSessionPerms, CredentialType, IntentTokenState};
use compact_jwt::compact::JweCompact;
use compact_jwt::jwe::JweBuilder;
use super::accountpolicy::ResolvedAccountPolicy;
@ -59,7 +61,7 @@ struct CredentialUpdateSessionTokenInner {
#[derive(Debug)]
pub struct CredentialUpdateSessionToken {
pub token_enc: String,
pub token_enc: JweCompact,
}
/// The current state of MFA registration
@ -850,10 +852,6 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
mfaregstate: MfaRegState::None,
};
let status: CredentialUpdateSessionStatus = (&session).into();
let session = Arc::new(Mutex::new(session));
let max_ttl = ct + MAXIMUM_CRED_UPDATE_TTL;
let token = CredentialUpdateSessionTokenInner { sessionid, max_ttl };
@ -863,7 +861,16 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
OperationError::SerdeJsonError
})?;
let token_enc = self.domain_keys.token_enc_key.encrypt(&token_data);
let token_jwe = JweBuilder::from(token_data).build();
let token_enc = self
.qs_write
.get_domain_key_object_handle()?
.jwe_a128gcm_encrypt(&token_jwe, ct)?;
let status: CredentialUpdateSessionStatus = (&session).into();
let session = Arc::new(Mutex::new(session));
// Point of no return
@ -1195,15 +1202,15 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
OperationError,
> {
let session_token: CredentialUpdateSessionTokenInner = self
.domain_keys
.token_enc_key
.decrypt(&cust.token_enc)
.qs_write
.get_domain_key_object_handle()?
.jwe_decrypt(&cust.token_enc)
.map_err(|e| {
admin_error!(?e, "Failed to decrypt credential update session request");
OperationError::SessionExpired
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
data.from_json().map_err(|e| {
admin_error!(err = ?e, "Failed to deserialise credential update session request");
OperationError::SerdeJsonError
})
@ -1492,15 +1499,15 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
ct: Duration,
) -> Result<CredentialUpdateSessionMutex, OperationError> {
let session_token: CredentialUpdateSessionTokenInner = self
.domain_keys
.token_enc_key
.decrypt(&cust.token_enc)
.qs_read
.get_domain_key_object_handle()?
.jwe_decrypt(&cust.token_enc)
.map_err(|e| {
admin_error!(?e, "Failed to decrypt credential update session request");
OperationError::SessionExpired
})
.and_then(|data| {
serde_json::from_slice(&data).map_err(|e| {
data.from_json().map_err(|e| {
admin_error!(err = ?e, "Failed to deserialise credential update session request");
OperationError::SerdeJsonError
})
@ -2310,6 +2317,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
#[cfg(test)]
mod tests {
use compact_jwt::JwsCompact;
use std::time::Duration;
use kanidm_proto::internal::{CUExtPortal, CredentialDetailType, PasswordFeedback};
@ -2545,7 +2553,7 @@ mod tests {
idms_delayed: &mut IdmServerDelayed,
pw: &str,
ct: Duration,
) -> Option<String> {
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await;
let auth_init = AuthEvent::named_init("testperson");
@ -2599,7 +2607,7 @@ mod tests {
pw: &str,
token: &Totp,
ct: Duration,
) -> Option<String> {
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await;
let auth_init = AuthEvent::named_init("testperson");
@ -2665,7 +2673,7 @@ mod tests {
pw: &str,
code: &str,
ct: Duration,
) -> Option<String> {
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await;
let auth_init = AuthEvent::named_init("testperson");
@ -2733,7 +2741,7 @@ mod tests {
wa: &mut WebauthnAuthenticator<T>,
origin: Url,
ct: Duration,
) -> Option<String> {
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await;
let auth_init = AuthEvent::named_init("testperson");

View file

@ -1,5 +1,6 @@
use crate::idm::AuthState;
use crate::prelude::*;
use compact_jwt::JwsCompact;
use kanidm_proto::v1::{AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthStep};
#[cfg(test)]
@ -266,11 +267,11 @@ impl LdapAuthEvent {
}
pub struct LdapTokenAuthEvent {
pub token: String,
pub token: JwsCompact,
}
impl LdapTokenAuthEvent {
pub fn from_parts(token: String) -> Result<Self, OperationError> {
pub fn from_parts(token: JwsCompact) -> Result<Self, OperationError> {
Ok(LdapTokenAuthEvent { token })
}
}

View file

@ -3,7 +3,9 @@
use std::collections::BTreeSet;
use std::iter;
use std::str::FromStr;
use compact_jwt::JwsCompact;
use kanidm_proto::constants::*;
use kanidm_proto::internal::{ApiToken, UserAuthToken};
use ldap3_proto::simple::*;
@ -429,7 +431,12 @@ impl LdapServer {
idm_auth.auth_ldap(&lae, ct).await?
}
LdapBindTarget::ApiToken => {
let lae = LdapTokenAuthEvent::from_parts(pw.to_string())?;
let jwsc = JwsCompact::from_str(pw).map_err(|err| {
error!(?err, "Invalid JwsCompact supplied as authentication token.");
OperationError::NotAuthenticated
})?;
let lae = LdapTokenAuthEvent::from_parts(jwsc)?;
idm_auth.token_auth_ldap(&lae, ct).await?
}
};
@ -617,9 +624,8 @@ pub(crate) fn ldap_attr_filter_map(input: &str) -> AttrString {
#[cfg(test)]
mod tests {
use crate::prelude::*;
use std::str::FromStr;
use compact_jwt::{JwsCompact, JwsEs256Verifier, JwsVerifier};
use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
use hashbrown::HashSet;
use kanidm_proto::internal::ApiToken;
use ldap3_proto::proto::{LdapFilter, LdapOp, LdapSearchScope, LdapSubstringFilter};
@ -1133,28 +1139,28 @@ mod tests {
};
// Inspect the token to get its uuid out.
let apitoken_unverified =
JwsCompact::from_str(&apitoken).expect("Failed to parse apitoken");
let jws_verifier =
JwsEs256Verifier::try_from(apitoken_unverified.get_jwk_pubkey().unwrap()).unwrap();
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
let apitoken_inner = jws_verifier
.verify(&apitoken_unverified)
.verify(&apitoken)
.unwrap()
.from_json::<ApiToken>()
.unwrap();
// Bind using the token as a DN
let sa_lbt = ldaps
.do_bind(idms, "dn=token", &apitoken)
.do_bind(idms, "dn=token", &apitoken.to_string())
.await
.unwrap()
.unwrap();
assert!(sa_lbt.effective_session == LdapSession::ApiToken(apitoken_inner.clone()));
// Bind using the token as a pw
let sa_lbt = ldaps.do_bind(idms, "", &apitoken).await.unwrap().unwrap();
let sa_lbt = ldaps
.do_bind(idms, "", &apitoken.to_string())
.await
.unwrap()
.unwrap();
assert!(sa_lbt.effective_session == LdapSession::ApiToken(apitoken_inner));
// Search and retrieve mail that's now accessible.

View file

@ -23,6 +23,7 @@ pub mod serviceaccount;
pub(crate) mod unix;
use crate::server::identity::Source;
use compact_jwt::JwsCompact;
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
use std::fmt;
@ -30,7 +31,7 @@ pub enum AuthState {
Choose(Vec<AuthMech>),
Continue(Vec<AuthAllowed>),
Denied(String),
Success(String, AuthIssueSession),
Success(JwsCompact, AuthIssueSession),
}
impl fmt::Debug for AuthState {
@ -48,7 +49,8 @@ impl fmt::Debug for AuthState {
pub struct ClientAuthInfo {
pub source: Source,
pub client_cert: Option<ClientCertInfo>,
pub bearer_token: Option<String>,
pub bearer_token: Option<JwsCompact>,
pub basic_authz: Option<String>,
}
#[derive(Debug, Clone)]
@ -57,6 +59,18 @@ pub struct ClientCertInfo {
pub cn: Option<String>,
}
#[cfg(test)]
impl ClientAuthInfo {
fn none() -> Self {
ClientAuthInfo {
source: Source::Internal,
client_cert: None,
bearer_token: None,
basic_authz: None,
}
}
}
#[cfg(test)]
impl From<Source> for ClientAuthInfo {
fn from(value: Source) -> ClientAuthInfo {
@ -64,17 +78,19 @@ impl From<Source> for ClientAuthInfo {
source: value,
client_cert: None,
bearer_token: None,
basic_authz: None,
}
}
}
#[cfg(test)]
impl From<&str> for ClientAuthInfo {
fn from(value: &str) -> ClientAuthInfo {
impl From<JwsCompact> for ClientAuthInfo {
fn from(value: JwsCompact) -> ClientAuthInfo {
ClientAuthInfo {
source: Source::Internal,
client_cert: None,
bearer_token: Some(value.to_string()),
bearer_token: Some(value),
basic_authz: None,
}
}
}
@ -86,6 +102,34 @@ impl From<ClientCertInfo> for ClientAuthInfo {
source: Source::Internal,
client_cert: Some(value),
bearer_token: None,
basic_authz: None,
}
}
}
#[cfg(test)]
impl From<&str> for ClientAuthInfo {
fn from(value: &str) -> ClientAuthInfo {
ClientAuthInfo {
source: Source::Internal,
client_cert: None,
bearer_token: None,
basic_authz: Some(value.to_string()),
}
}
}
#[cfg(test)]
impl ClientAuthInfo {
fn encode_basic(id: &str, secret: &str) -> ClientAuthInfo {
use base64::{engine::general_purpose, Engine as _};
let value = format!("{id}:{secret}");
let value = general_purpose::STANDARD.encode(&value);
ClientAuthInfo {
source: Source::Internal,
client_cert: None,
bearer_token: None,
basic_authz: Some(value),
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -137,8 +137,11 @@ impl<'a> IdmServerAuthTransaction<'a> {
ct,
client_auth_info,
};
let domain_keys = self.qs_read.get_domain_key_object_handle()?;
let (auth_session, state) =
AuthSession::new_reauth(asd, ident.session_id, session, session_cred_id);
AuthSession::new_reauth(asd, ident.session_id, session, session_cred_id, domain_keys);
// Push the re-auth session to the session maps.
match auth_session {
@ -178,6 +181,7 @@ mod tests {
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech};
use compact_jwt::JwsCompact;
use uuid::uuid;
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
@ -324,7 +328,7 @@ mod tests {
ct: Duration,
wa: &mut WebauthnAuthenticator<SoftPasskey>,
idms_delayed: &mut IdmServerDelayed,
) -> Option<String> {
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await;
let origin = idms_auth.get_origin().clone();
@ -404,7 +408,7 @@ mod tests {
pw: &str,
token: &Totp,
idms_delayed: &mut IdmServerDelayed,
) -> Option<String> {
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await;
let auth_init = AuthEvent::named_init("testperson");
@ -487,7 +491,7 @@ mod tests {
ident: &Identity,
wa: &mut WebauthnAuthenticator<SoftPasskey>,
idms_delayed: &mut IdmServerDelayed,
) -> Option<String> {
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await;
let origin = idms_auth.get_origin().clone();
@ -554,7 +558,7 @@ mod tests {
pw: &str,
token: &Totp,
idms_delayed: &mut IdmServerDelayed,
) -> Option<String> {
) -> Option<JwsCompact> {
let mut idms_auth = idms.auth().await;
let auth_allowed = idms_auth
@ -631,7 +635,7 @@ mod tests {
.expect("failed to authenticate with passkey");
// Token_str to uat
let ident = token_to_ident(idms, ct, token.as_str().into()).await;
let ident = token_to_ident(idms, ct, token.clone().into()).await;
// Check that the rw entitlement is not present
debug!(?ident);
@ -650,7 +654,7 @@ mod tests {
.expect("Failed to get new session token");
// Token_str to uat
let ident = token_to_ident(idms, ct, token.as_str().into()).await;
let ident = token_to_ident(idms, ct, token.clone().into()).await;
// They now have the entitlement.
debug!(?ident);
@ -677,7 +681,7 @@ mod tests {
.expect("failed to authenticate with passkey");
// Token_str to uat
let ident = token_to_ident(idms, ct, token.as_str().into()).await;
let ident = token_to_ident(idms, ct, token.into()).await;
// Check that the rw entitlement is not present
debug!(?ident);

View file

@ -2,7 +2,7 @@ use std::time::Duration;
use base64urlsafedata::Base64UrlSafeData;
use compact_jwt::{Jws, JwsEs256Signer, JwsSigner};
use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
use kanidm_proto::internal::{ApiTokenPurpose, ScimSyncToken};
use kanidm_proto::scim_v1::*;
use std::collections::{BTreeMap, BTreeSet};
@ -22,7 +22,7 @@ pub(crate) struct SyncAccount {
pub name: String,
pub uuid: Uuid,
pub sync_tokens: BTreeMap<Uuid, ApiToken>,
pub jws_key: JwsEs256Signer,
pub jws_key: Option<JwsEs256Signer>,
}
macro_rules! try_from_entry {
@ -44,11 +44,11 @@ macro_rules! try_from_entry {
let jws_key = $value
.get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
.cloned()
.ok_or(OperationError::InvalidAccountState(format!(
"Missing attribute: {}",
Attribute::JwsEs256PrivateKey
)))?
.set_sign_option_embed_jwk(true);
.map(|jws_key| {
jws_key
.set_sign_option_embed_jwk(true)
.set_sign_option_legacy_kid(true)
});
let sync_tokens = $value
.get_ava_as_apitoken_map(Attribute::SyncTokenSession)
@ -123,7 +123,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
&mut self,
gte: &GenerateScimSyncTokenEvent,
ct: Duration,
) -> Result<String, OperationError> {
) -> Result<JwsCompact, OperationError> {
// Get the target signing key.
let sync_account = self
.qs_write
@ -182,21 +182,31 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// Provide the event to impersonate
&gte.ident,
)
.and_then(|_| {
// The modify succeeded and was allowed, now sign the token for return.
sync_account
.jws_key
.sign(&token)
.map(|jws_signed| jws_signed.to_string())
.map_err(|e| {
admin_error!(err = ?e, "Unable to sign sync token");
.map_err(|err| {
error!(?err, "Failed to generate sync token");
err
})?;
// The modify succeeded and was allowed, now sign the token for return.
if self.qs_write.get_domain_version() < DOMAIN_LEVEL_6 {
sync_account
.jws_key
.as_ref()
.ok_or_else(|| {
admin_error!("Unable to sign sync token, no sync keys available");
OperationError::CryptographyError
})
.and_then(|jws_key| {
jws_key.sign(&token).map_err(|err| {
admin_error!(?err, "Unable to sign sync token");
OperationError::CryptographyError
})
})
.map_err(|e| {
admin_error!("Failed to generate sync token {:?}", e);
e
})
})
} else {
self.qs_write
.get_domain_key_object_handle()?
.jws_es256_sign(&token, ct)
}
// Done!
}
@ -1534,8 +1544,11 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
mod tests {
use crate::idm::server::{IdmServerProxyWriteTransaction, IdmServerTransaction};
use crate::prelude::*;
use crate::server::keys::KeyProvidersTransaction;
use crate::value::KeyStatus;
use base64urlsafedata::Base64UrlSafeData;
use compact_jwt::{Jws, JwsSigner};
use compact_jwt::traits::JwsVerifiable;
use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
use kanidm_proto::internal::ApiTokenPurpose;
use kanidm_proto::scim_v1::*;
use std::sync::Arc;
@ -1551,7 +1564,7 @@ mod tests {
fn create_scim_sync_account(
idms_prox_write: &mut IdmServerProxyWriteTransaction<'_>,
ct: Duration,
) -> (Uuid, String) {
) -> (Uuid, JwsCompact) {
let sync_uuid = Uuid::new_v4();
let e1 = entry_init!(
@ -1565,9 +1578,10 @@ mod tests {
)
);
let ce = CreateEvent::new_internal(vec![e1]);
let cr = idms_prox_write.qs_write.create(&ce);
assert!(cr.is_ok());
idms_prox_write
.qs_write
.internal_create(vec![e1])
.expect("Failed to create sync account");
let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
@ -1594,7 +1608,7 @@ mod tests {
let mut idms_prox_read = idms.proxy_read().await;
let ident = idms_prox_read
.validate_sync_client_auth_info_to_ident(sync_token.as_str().into(), ct)
.validate_sync_client_auth_info_to_ident(sync_token.into(), ct)
.expect("Failed to validate sync token");
assert!(Some(sync_uuid) == ident.get_uuid());
@ -1650,7 +1664,7 @@ mod tests {
// -- Check the happy path.
let mut idms_prox_read = idms.proxy_read().await;
let ident = idms_prox_read
.validate_sync_client_auth_info_to_ident(sync_token.as_str().into(), ct)
.validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct)
.expect("Failed to validate sync token");
assert!(Some(sync_uuid) == ident.get_uuid());
drop(idms_prox_read);
@ -1671,7 +1685,7 @@ mod tests {
// Must fail
let mut idms_prox_read = idms.proxy_read().await;
let fail =
idms_prox_read.validate_sync_client_auth_info_to_ident(sync_token.as_str().into(), ct);
idms_prox_read.validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct);
assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
drop(idms_prox_read);
@ -1683,19 +1697,24 @@ mod tests {
.scim_sync_generate_token(&gte, ct)
.expect("failed to generate new scim sync token");
let me_inv_m = ModifyEvent::new_internal_invalid(
filter!(f_eq(
Attribute::Name,
PartialValue::new_iname("test_scim_sync")
)),
ModifyList::new_list(vec![Modify::Purged(Attribute::JwsEs256PrivateKey.into())]),
);
assert!(idms_prox_write.qs_write.modify(&me_inv_m).is_ok());
let revoke_kid = sync_token.kid().expect("token does not contain a key id");
idms_prox_write
.qs_write
.internal_modify_uuid(
UUID_DOMAIN_INFO,
&ModifyList::new_append(
Attribute::KeyActionRevoke.into(),
Value::HexString(revoke_kid.to_string()),
),
)
.expect("Unable to revoke key");
assert!(idms_prox_write.commit().is_ok());
let mut idms_prox_read = idms.proxy_read().await;
let fail =
idms_prox_read.validate_sync_client_auth_info_to_ident(sync_token.as_str().into(), ct);
idms_prox_read.validate_sync_client_auth_info_to_ident(sync_token.clone().into(), ct);
assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
// -- Forge a session, use wrong types
@ -1705,11 +1724,6 @@ mod tests {
.internal_search_uuid(sync_uuid)
.expect("Unable to access sync entry");
let jws_key = sync_entry
.get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
.cloned()
.expect("Missing attribute: jws_es256_private_key");
let sync_tokens = sync_entry
.get_ava_as_apitoken_map(Attribute::SyncTokenSession)
.cloned()
@ -1732,16 +1746,125 @@ mod tests {
let token = Jws::into_json(&scim_sync_token).expect("Unable to serialise forged token");
let forged_token = jws_key
.sign(&token)
.map(|jws_signed| jws_signed.to_string())
.expect("Unable to sign forged token");
let jws_key = JwsEs256Signer::generate_es256().expect("Unable to create signer");
let fail = idms_prox_read
.validate_sync_client_auth_info_to_ident(forged_token.as_str().into(), ct);
let forged_token = jws_key.sign(&token).expect("Unable to sign forged token");
let fail = idms_prox_read.validate_sync_client_auth_info_to_ident(forged_token.into(), ct);
assert!(matches!(fail, Err(OperationError::NotAuthenticated)));
}
#[idm_test(domain_level=DOMAIN_LEVEL_5)]
async fn test_idm_scim_sync_token_dl5_dl6_token(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let mut idms_prox_write = idms.proxy_write(ct).await;
assert_eq!(
idms_prox_write.qs_write.get_domain_version(),
DOMAIN_LEVEL_5
);
let sync_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::SyncAccount.to_value()),
(Attribute::Name, Value::new_iname("test_scim_sync")),
(Attribute::Uuid, Value::Uuid(sync_uuid)),
(
Attribute::Description,
Value::new_utf8s("A test sync agreement")
)
);
idms_prox_write
.qs_write
.internal_create(vec![e1])
.expect("Failed to create sync account");
let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
let old_sync_token = idms_prox_write
.scim_sync_generate_token(&gte, ct)
.expect("failed to generate new scim sync token");
assert!(idms_prox_write.commit().is_ok());
// Now trigger 5 -> 6
let mut idms_prox_write = idms.proxy_write(ct).await;
idms_prox_write
.qs_write
.internal_apply_domain_migration(DOMAIN_LEVEL_6)
.expect("Unable to set domain level to version 6");
assert!(idms_prox_write.commit().is_ok());
// Check existing token still validates.
let mut idms_prox_write = idms.proxy_write(ct).await;
let _ident = idms_prox_write
.validate_sync_client_auth_info_to_ident(old_sync_token.clone().into(), ct)
.expect("Failed to process old sync token to ident");
// Delete the old session else we get a schema violation
let modlist = ModifyList::new_purge(Attribute::SyncTokenSession.into());
idms_prox_write
.qs_write
.internal_modify_uuid(sync_uuid, &modlist)
.expect("Unable to delete previous sync token session");
let gte = GenerateScimSyncTokenEvent::new_internal(sync_uuid, "Sync Connector");
let new_sync_token = idms_prox_write
.scim_sync_generate_token(&gte, ct)
.expect("failed to generate new scim sync token");
assert_ne!(old_sync_token.kid(), new_sync_token.kid());
let _ident = idms_prox_write
.validate_sync_client_auth_info_to_ident(new_sync_token.into(), ct)
.expect("Failed to process new sync token to ident");
// The former key is now on the domain object.
let key_object = idms_prox_write
.qs_write
.get_key_providers()
.get_key_object(UUID_DOMAIN_INFO)
.expect("Unable to retrieve key object by uuid");
// Assert the former key is now in the domain key object, and now is "retained".
let former_kid = old_sync_token.kid().unwrap().to_string();
let status = key_object
.kid_status(&former_kid)
.expect("Failed to access kid status");
assert_eq!(status, Some(KeyStatus::Retained));
assert!(idms_prox_write.commit().is_ok());
// Now trigger 6 -> 7
let mut idms_prox_write = idms.proxy_write(ct).await;
idms_prox_write
.qs_write
.internal_apply_domain_migration(DOMAIN_LEVEL_7)
.expect("Unable to set domain level to version 7");
assert!(idms_prox_write.commit().is_ok());
// The key on the service account is removed.
let mut idms_prox_write = idms.proxy_write(ct).await;
let sync_entry = idms_prox_write
.qs_write
.internal_search_uuid(sync_uuid)
.expect("Unable to access service account");
assert!(!sync_entry.attribute_pres(Attribute::JwsEs256PrivateKey));
assert!(idms_prox_write.commit().is_ok());
}
fn test_scim_sync_apply_setup_ident(
idms_prox_write: &mut IdmServerProxyWriteTransaction,
ct: Duration,
@ -1770,7 +1893,7 @@ mod tests {
.expect("failed to generate new scim sync token");
let ident = idms_prox_write
.validate_sync_client_auth_info_to_ident(sync_token.as_str().into(), ct)
.validate_sync_client_auth_info_to_ident(sync_token.into(), ct)
.expect("Failed to process sync token to ident");
(sync_uuid, ident)

View file

@ -1,16 +1,12 @@
use std::convert::TryFrom;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use kanidm_lib_crypto::CryptoPolicy;
use compact_jwt::{JwsCompact, JwsEs256Signer, JwsEs256Verifier, JwsSignerToVerifier, JwsVerifier};
use compact_jwt::{Jwk, JwsCompact};
use concread::bptree::{BptreeMap, BptreeMapReadTxn, BptreeMapWriteTxn};
use concread::cowcell::{CowCellReadTxn, CowCellWriteTxn};
use concread::hashmap::HashMap;
use concread::CowCell;
use fernet::Fernet;
use kanidm_proto::internal::{
ApiToken, BackupCodesView, CredentialStatus, PasswordFeedback, RadiusAuthToken, ScimSyncToken,
UatPurpose, UserAuthToken,
@ -36,6 +32,7 @@ use crate::idm::delayed::{
AuthSessionRecord, BackupCodeRemoval, DelayedAction, PasswordUpgrade, UnixPasswordUpgrade,
WebauthnCounterIncrement,
};
#[cfg(test)]
use crate::idm::event::PasswordChangeEvent;
use crate::idm::event::{AuthEvent, AuthEventStep, AuthResult};
@ -54,20 +51,13 @@ use crate::idm::serviceaccount::ServiceAccount;
use crate::idm::unix::{UnixGroup, UnixUserAccount};
use crate::idm::AuthState;
use crate::prelude::*;
use crate::server::keys::KeyProvidersTransaction;
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
use crate::value::{Session, SessionState};
pub(crate) type AuthSessionMutex = Arc<Mutex<AuthSession>>;
pub(crate) type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
#[derive(Clone)]
pub struct DomainKeys {
pub(crate) uat_jwt_signer: JwsEs256Signer,
pub(crate) uat_jwt_validator: JwsEs256Verifier,
pub(crate) token_enc_key: Fernet,
pub(crate) cookie_key: [u8; 64],
}
pub struct IdmServer {
// There is a good reason to keep this single thread - it
// means that limits to sessions can be easily applied and checked to
@ -87,7 +77,6 @@ pub struct IdmServer {
/// [Webauthn] verifier/config
webauthn: Webauthn,
oauth2rs: Arc<Oauth2ResourceServers>,
domain_keys: Arc<CowCell<DomainKeys>>,
}
/// Contains methods that require writes, but in the context of writing to the idm in memory structures (maybe the query server too). This is things like authentication.
@ -103,7 +92,6 @@ pub struct IdmServerAuthTransaction<'a> {
pub(crate) async_tx: Sender<DelayedAction>,
pub(crate) audit_tx: Sender<AuditEvent>,
pub(crate) webauthn: &'a Webauthn,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
}
pub struct IdmServerCredUpdateTransaction<'a> {
@ -111,14 +99,12 @@ pub struct IdmServerCredUpdateTransaction<'a> {
// sid: Sid,
pub(crate) webauthn: &'a Webauthn,
pub(crate) cred_update_sessions: BptreeMapReadTxn<'a, Uuid, CredentialUpdateSessionMutex>,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
pub(crate) crypto_policy: &'a CryptoPolicy,
}
/// This contains read-only methods, like getting users, groups and other structured content.
pub struct IdmServerProxyReadTransaction<'a> {
pub qs_read: QueryServerReadTransaction<'a>,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
pub(crate) oauth2rs: Oauth2ResourceServersReadTransaction,
}
@ -131,7 +117,6 @@ pub struct IdmServerProxyWriteTransaction<'a> {
pub(crate) sid: Sid,
crypto_policy: &'a CryptoPolicy,
webauthn: &'a Webauthn,
pub(crate) domain_keys: CowCellWriteTxn<'a, DomainKeys>,
pub(crate) oauth2rs: Oauth2ResourceServersWriteTransaction<'a>,
}
@ -155,14 +140,11 @@ impl IdmServer {
let (audit_tx, audit_rx) = unbounded();
// Get the domain name, as the relying party id.
let (rp_id, rp_name, fernet_private_key, es256_private_key, cookie_key, oauth2rs_set) = {
let (rp_id, rp_name, oauth2rs_set) = {
let mut qs_read = qs.read().await;
(
qs_read.get_domain_name().to_string(),
qs_read.get_domain_display_name().to_string(),
qs_read.get_domain_fernet_private_key()?,
qs_read.get_domain_es256_private_key()?,
qs_read.get_domain_cookie_key()?,
// Add a read/reload of all oauth2 configurations.
qs_read.get_oauth2rs_set()?,
)
@ -199,31 +181,6 @@ impl IdmServer {
OperationError::InvalidState
})?;
// Setup our auth token signing key.
let token_enc_key = Fernet::new(&fernet_private_key).ok_or_else(|| {
admin_error!("Unable to load Fernet encryption key");
OperationError::CryptographyError
})?;
let uat_jwt_signer = JwsEs256Signer::from_es256_der(&es256_private_key)
.map_err(|e| {
admin_error!(err = ?e, "Unable to load ES256 JwsSigner from DER");
OperationError::CryptographyError
})?
.set_sign_option_embed_jwk(true);
let uat_jwt_validator = uat_jwt_signer.get_verifier().map_err(|e| {
admin_error!(err = ?e, "Unable to load ES256 JwsValidator from JwsSigner");
OperationError::CryptographyError
})?;
let domain_keys = Arc::new(CowCell::new(DomainKeys {
uat_jwt_signer,
uat_jwt_validator,
token_enc_key,
cookie_key,
}));
let oauth2rs =
Oauth2ResourceServers::try_from((oauth2rs_set, origin_url)).map_err(|e| {
admin_error!("Failed to load oauth2 resource servers - {:?}", e);
@ -241,7 +198,6 @@ impl IdmServer {
async_tx,
audit_tx,
webauthn,
domain_keys,
oauth2rs: Arc::new(oauth2rs),
},
IdmServerDelayed { async_rx },
@ -249,10 +205,6 @@ impl IdmServer {
))
}
pub fn get_cookie_key(&self) -> [u8; 64] {
self.domain_keys.read().cookie_key
}
/// Start an auth txn
pub async fn auth(&self) -> IdmServerAuthTransaction<'_> {
let qs_read = self.qs.read().await;
@ -270,7 +222,6 @@ impl IdmServer {
async_tx: self.async_tx.clone(),
audit_tx: self.audit_tx.clone(),
webauthn: &self.webauthn,
domain_keys: self.domain_keys.read(),
}
}
@ -279,7 +230,6 @@ impl IdmServer {
pub async fn proxy_read(&self) -> IdmServerProxyReadTransaction<'_> {
IdmServerProxyReadTransaction {
qs_read: self.qs.read().await,
domain_keys: self.domain_keys.read(),
oauth2rs: self.oauth2rs.read(),
// async_tx: self.async_tx.clone(),
}
@ -299,7 +249,6 @@ impl IdmServer {
sid,
crypto_policy: &self.crypto_policy,
webauthn: &self.webauthn,
domain_keys: self.domain_keys.write(),
oauth2rs: self.oauth2rs.write(),
}
}
@ -310,7 +259,6 @@ impl IdmServer {
// sid: Sid,
webauthn: &self.webauthn,
cred_update_sessions: self.cred_update_sessions.read(),
domain_keys: self.domain_keys.read(),
crypto_policy: &self.crypto_policy,
}
}
@ -397,8 +345,6 @@ pub trait IdmServerTransaction<'a> {
fn get_qs_txn(&mut self) -> &mut Self::QsTransactionType;
fn get_uat_validator_txn(&self) -> &JwsEs256Verifier;
/// 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
@ -414,9 +360,10 @@ pub trait IdmServerTransaction<'a> {
ct: Duration,
) -> Result<Identity, OperationError> {
let ClientAuthInfo {
source,
client_cert,
bearer_token,
source,
basic_authz: _,
} = client_auth_info;
match (client_cert, bearer_token) {
@ -448,6 +395,7 @@ pub trait IdmServerTransaction<'a> {
client_cert,
bearer_token,
source: _,
basic_authz: _,
} = client_auth_info;
match (client_cert, bearer_token) {
@ -472,103 +420,39 @@ pub trait IdmServerTransaction<'a> {
fn validate_and_parse_token_to_token(
&mut self,
token: &str,
jwsu: &JwsCompact,
ct: Duration,
) -> Result<Token, OperationError> {
let jwsu = JwsCompact::from_str(token).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_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 = jws_validator
.verify(&jwsu)
.map_err(|e| {
security_info!(?e, "Unable to verify token");
OperationError::NotAuthenticated
})
.and_then(|t| {
t.from_json::<UserAuthToken>().map_err(|err| {
error!(?err, "Unable to deserialise JWS");
OperationError::SerdeJsonError
})
})?;
if let Some(exp) = uat.expiry {
let ct_odt = time::OffsetDateTime::UNIX_EPOCH + ct;
if ct_odt >= exp {
security_info!(?ct_odt, ?exp, "Session expired");
Err(OperationError::SessionExpired)
} else {
trace!(?ct_odt, ?exp, "Session not yet expired");
Ok(Token::UserAuthToken(uat))
}
} else {
debug!("Session has no expiry");
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(
Attribute::JwsEs256PrivateKey,
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(Attribute::JwsEs256PrivateKey)
.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_verifier().map_err(|e| {
security_info!(?e, "Unable to access token verifier");
// Our key objects now handle this logic and determine the correct key
// from the input type.
let jws_inner = self
.get_qs_txn()
.get_domain_key_object_handle()?
.jws_verify(jwsu)
.map_err(|err| {
security_info!(?err, "Unable to verify token");
OperationError::NotAuthenticated
})?;
let apit = user_validator
.verify(&jwsu)
.map_err(|e| {
security_info!(?e, "Unable to verify token");
OperationError::NotAuthenticated
})
.and_then(|t| {
t.from_json::<ApiToken>().map_err(|err| {
error!(?err, "Unable to deserialise JWS");
OperationError::SerdeJsonError
})
})?;
// Is it a UAT?
if let Ok(uat) = jws_inner.from_json::<UserAuthToken>() {
if let Some(exp) = uat.expiry {
let ct_odt = time::OffsetDateTime::UNIX_EPOCH + ct;
if exp < ct_odt {
security_info!(?ct_odt, ?exp, "Session expired");
return Err(OperationError::SessionExpired);
} else {
trace!(?ct_odt, ?exp, "Session not yet expired");
return Ok(Token::UserAuthToken(uat));
}
} else {
debug!("Session has no expiry");
return Ok(Token::UserAuthToken(uat));
}
};
// Is it an API Token?
if let Ok(apit) = jws_inner.from_json::<ApiToken>() {
if let Some(expiry) = apit.expiry {
if time::OffsetDateTime::UNIX_EPOCH + ct >= expiry {
security_info!("Session expired");
@ -576,53 +460,19 @@ pub trait IdmServerTransaction<'a> {
}
}
Ok(Token::ApiToken(apit, entry))
}
}
#[instrument(level = "debug", skip_all)]
fn validate_and_parse_uat(
&self,
token: Option<&str>,
ct: Duration,
) -> Result<UserAuthToken, OperationError> {
// Given the token string, validate and recreate the UAT
let jws_validator = self.get_uat_validator_txn();
let uat: UserAuthToken = token
.ok_or(OperationError::NotAuthenticated)
.and_then(|s| {
JwsCompact::from_str(s).map_err(|e| {
security_info!(?e, "Unable to decode token");
let entry = self
.get_qs_txn()
.internal_search_uuid(apit.account_id)
.map_err(|err| {
security_info!(?err, "Account associated with api token no longer exists.");
OperationError::NotAuthenticated
})
})
.and_then(|jwtu| {
jws_validator
.verify(&jwtu)
.map_err(|e| {
security_info!(?e, "Unable to verify token");
OperationError::NotAuthenticated
})
.and_then(|t| {
t.from_json::<UserAuthToken>().map_err(|err| {
error!(?err, "Unable to deserialise JWS");
OperationError::SerdeJsonError
})
})
})?;
})?;
if let Some(exp) = uat.expiry {
if time::OffsetDateTime::UNIX_EPOCH + ct >= exp {
security_info!("Session expired");
Err(OperationError::SessionExpired)
} else {
Ok(uat)
}
} else {
debug!("Session has no expiry");
Ok(uat)
}
return Ok(Token::ApiToken(apit, entry));
};
security_info!("Unable to verify token, invalid inner JSON");
Err(OperationError::NotAuthenticated)
}
fn check_oauth2_account_uuid_valid(
@ -901,70 +751,42 @@ pub trait IdmServerTransaction<'a> {
) -> Result<Identity, OperationError> {
// FUTURE: Could allow mTLS here instead?
let jwsu = client_auth_info
.bearer_token
.ok_or_else(|| {
security_info!("No token provided");
let jwsu = client_auth_info.bearer_token.ok_or_else(|| {
security_info!("No token provided");
OperationError::NotAuthenticated
})?;
let jws_inner = self
.get_qs_txn()
.get_domain_key_object_handle()?
.jws_verify(&jwsu)
.map_err(|err| {
security_info!(?err, "Unable to verify token");
OperationError::NotAuthenticated
})
.and_then(|s| {
JwsCompact::from_str(s.as_str()).map_err(|e| {
security_info!(?e, "Unable to decode token");
OperationError::NotAuthenticated
})
})?;
let kid = jwsu.get_jwk_kid().ok_or_else(|| {
security_info!("Token does not contain a valid kid");
OperationError::NotAuthenticated
let sync_token = jws_inner.from_json::<ScimSyncToken>().map_err(|err| {
error!(?err, "Unable to deserialise JWS");
OperationError::SerdeJsonError
})?;
let entry = self
.get_qs_txn()
.internal_search(filter!(f_eq(
Attribute::JwsEs256PrivateKey,
PartialValue::new_iutf8(kid)
Attribute::SyncTokenSession,
PartialValue::Refer(sync_token.token_id)
)))
.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"
token_id = ?sync_token.token_id,
"entries was empty, or matched multiple results for token id"
);
Err(OperationError::NotAuthenticated)
}
})?;
let user_signer = entry
.get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
.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_verifier().map_err(|e| {
security_info!(?e, "Unable to access token verifier");
OperationError::NotAuthenticated
})?;
let sync_token = user_validator
.verify(&jwsu)
.map_err(|e| {
security_info!(?e, "Unable to verify token");
OperationError::NotAuthenticated
})
.and_then(|t| {
t.from_json::<ScimSyncToken>().map_err(|err| {
error!(?err, "Unable to deserialise JWS");
OperationError::SerdeJsonError
})
})?;
let valid = SyncAccount::check_sync_token_valid(ct, &sync_token, &entry);
if !valid {
@ -992,10 +814,6 @@ impl<'a> IdmServerTransaction<'a> for IdmServerAuthTransaction<'a> {
fn get_qs_txn(&mut self) -> &mut Self::QsTransactionType {
&mut self.qs_read
}
fn get_uat_validator_txn(&self) -> &JwsEs256Verifier {
&self.domain_keys.uat_jwt_validator
}
}
impl<'a> IdmServerAuthTransaction<'a> {
@ -1112,7 +930,9 @@ impl<'a> IdmServerAuthTransaction<'a> {
client_auth_info,
};
let (auth_session, state) = AuthSession::new(asd, init.privileged);
let domain_keys = self.qs_read.get_domain_key_object_handle()?;
let (auth_session, state) = AuthSession::new(asd, init.privileged, domain_keys);
match auth_session {
Some(auth_session) => {
@ -1236,7 +1056,6 @@ impl<'a> IdmServerAuthTransaction<'a> {
&self.async_tx,
&self.audit_tx,
self.webauthn,
&self.domain_keys.uat_jwt_signer,
self.qs_read.pw_badlist(),
)
.map(|aus| {
@ -1494,11 +1313,6 @@ impl<'a> IdmServerAuthTransaction<'a> {
}
pub fn commit(self) -> Result<(), OperationError> {
/*
lperf_trace_segment!("idm::server::IdmServerAuthTransaction::commit", || {
self.sessions.commit();
Ok(())
})*/
Ok(())
}
}
@ -1509,13 +1323,19 @@ impl<'a> IdmServerTransaction<'a> for IdmServerProxyReadTransaction<'a> {
fn get_qs_txn(&mut self) -> &mut Self::QsTransactionType {
&mut self.qs_read
}
fn get_uat_validator_txn(&self) -> &JwsEs256Verifier {
&self.domain_keys.uat_jwt_validator
}
}
impl<'a> IdmServerProxyReadTransaction<'a> {
pub fn jws_public_jwk(&mut self, key_id: &str) -> Result<Jwk, OperationError> {
self.qs_read
.get_key_providers()
.get_key_object_handle(UUID_DOMAIN_INFO)
// If there is no domain info, error.
.ok_or(OperationError::NoMatchingEntries)
.and_then(|key_object| key_object.jws_public_jwk(key_id))
.and_then(|maybe_key: Option<Jwk>| maybe_key.ok_or(OperationError::NoMatchingEntries))
}
pub fn get_radiusauthtoken(
&mut self,
rate: &RadiusAuthTokenEvent,
@ -1612,10 +1432,6 @@ impl<'a> IdmServerTransaction<'a> for IdmServerProxyWriteTransaction<'a> {
fn get_qs_txn(&mut self) -> &mut Self::QsTransactionType {
&mut self.qs_write
}
fn get_uat_validator_txn(&self) -> &JwsEs256Verifier {
&self.domain_keys.uat_jwt_validator
}
}
impl<'a> IdmServerProxyWriteTransaction<'a> {
@ -2099,60 +1915,14 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
self.qs_write
.get_oauth2rs_set()
.and_then(|oauth2rs_set| self.oauth2rs.reload(oauth2rs_set))?;
// Clear the flag to indicate we completed the reload.
self.qs_write.clear_changed_oauth2();
}
if self.qs_write.get_changed_domain() {
// reload token_key?
self.qs_write
.get_domain_fernet_private_key()
.and_then(|token_key| {
Fernet::new(&token_key).ok_or_else(|| {
admin_error!("Failed to generate token_enc_key");
OperationError::InvalidState
})
})
.map(|new_handle| {
self.domain_keys.token_enc_key = new_handle;
})?;
self.qs_write
.get_domain_es256_private_key()
.and_then(|key_der| {
JwsEs256Signer::from_es256_der(&key_der)
.map(|signer| signer.set_sign_option_embed_jwk(true))
.map_err(|e| {
admin_error!("Failed to generate uat_jwt_signer - {:?}", e);
OperationError::InvalidState
})
})
.and_then(|signer| {
signer
.get_verifier()
.map_err(|e| {
admin_error!("Failed to generate uat_jwt_validator - {:?}", e);
OperationError::InvalidState
})
.map(|validator| (signer, validator))
})
.map(|(new_signer, new_validator)| {
self.domain_keys.uat_jwt_signer = new_signer;
self.domain_keys.uat_jwt_validator = new_validator;
})?;
self.qs_write
.get_domain_cookie_key()
.map(|new_cookie_key| {
self.domain_keys.cookie_key = new_cookie_key;
})?;
// If the domain name has changed, we need to update rp-id in
// webauthn rs
//
// TODO: I'm not sure actually. because on a domain rename we
// might need to update origin too. So this gets a bit tricky.
// we might actually need to *not* reload here, and then let the
// admin do it inline with their configs too.
}
// Commit everything.
self.oauth2rs.commit();
self.domain_keys.commit();
self.cred_update_sessions.commit();
trace!("cred_update_session.commit");
self.qs_write.commit()
}
@ -2183,8 +1953,9 @@ mod tests {
use crate::idm::AuthState;
use crate::modify::{Modify, ModifyList};
use crate::prelude::*;
use crate::server::keys::KeyProvidersTransaction;
use crate::value::SessionState;
use compact_jwt::{JwsCompact, JwsEs256Verifier, JwsVerifier};
use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
use kanidm_lib_crypto::CryptoPolicy;
const TEST_PASSWORD: &str = "ntaoeuntnaoeuhraohuercahu😍";
@ -2415,20 +2186,15 @@ mod tests {
sessionid
}
async fn check_testperson_password(idms: &IdmServer, pw: &str) -> String {
let sid =
init_authsession_sid(idms, Duration::from_secs(TEST_CURRENT_TIME), "testperson1").await;
async fn check_testperson_password(idms: &IdmServer, pw: &str, ct: Duration) -> JwsCompact {
let sid = init_authsession_sid(idms, ct, "testperson1").await;
let mut idms_auth = idms.auth().await;
let anon_step = AuthEvent::cred_step_password(sid, pw);
// Expect success
let r2 = idms_auth
.auth(
&anon_step,
Duration::from_secs(TEST_CURRENT_TIME),
Source::Internal.into(),
)
.auth(&anon_step, ct, Source::Internal.into())
.await;
debug!("r2 ==> {:?}", r2);
@ -2464,10 +2230,11 @@ mod tests {
#[idm_test]
async fn test_idm_simple_password_auth(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
let ct = duration_from_epoch_now();
init_testperson_w_password(idms, TEST_PASSWORD)
.await
.expect("Failed to setup admin account");
check_testperson_password(idms, TEST_PASSWORD).await;
check_testperson_password(idms, TEST_PASSWORD, ct).await;
// Clear our the session record
let da = idms_delayed.try_recv().expect("invalid");
@ -2814,11 +2581,12 @@ mod tests {
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = duration_from_epoch_now();
// Assert the delayed action queue is empty
idms_delayed.check_is_empty_or_panic();
// Setup the admin w_ an imported password.
{
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await;
let mut idms_prox_write = idms.proxy_write(ct).await;
// now modify and provide a primary credential.
idms_prox_write
@ -2853,7 +2621,7 @@ mod tests {
drop(idms_prox_read);
// Do an auth, this will trigger the action to send.
check_testperson_password(idms, "password").await;
check_testperson_password(idms, "password", ct).await;
// ⚠️ We have to be careful here. Between these two actions, it's possible
// that on the pw upgrade that the credential uuid changes. This immediately
@ -2886,7 +2654,7 @@ mod tests {
assert_eq!(cred_before.uuid, cred_after.uuid);
// Check the admin pw still matches
check_testperson_password(idms, "password").await;
check_testperson_password(idms, "password", ct).await;
// Clear the next auth session record
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
@ -3472,7 +3240,7 @@ mod tests {
init_testperson_w_password(idms, TEST_PASSWORD)
.await
.expect("Failed to setup admin account");
let token = check_testperson_password(idms, TEST_PASSWORD).await;
let token = check_testperson_password(idms, TEST_PASSWORD, ct).await;
// Clear out the queued session record
let da = idms_delayed.try_recv().expect("invalid");
@ -3486,11 +3254,11 @@ mod tests {
// Check it's valid - This is within the time window so will pass.
idms_prox_read
.validate_client_auth_info_to_ident(token.as_str().into(), ct)
.validate_client_auth_info_to_ident(token.clone().into(), ct)
.expect("Failed to validate");
// In X time it should be INVALID
match idms_prox_read.validate_client_auth_info_to_ident(token.as_str().into(), expiry) {
match idms_prox_read.validate_client_auth_info_to_ident(token.into(), expiry) {
Err(OperationError::SessionExpired) => {}
_ => assert!(false),
}
@ -3593,9 +3361,8 @@ mod tests {
idms_delayed: &mut IdmServerDelayed,
) {
use kanidm_proto::internal::UserAuthToken;
use std::str::FromStr;
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let ct = duration_from_epoch_now();
let post_grace = ct + GRACE_WINDOW + Duration::from_secs(1);
let expiry = ct + Duration::from_secs(DEFAULT_AUTH_SESSION_EXPIRY as u64 + 1);
@ -3608,7 +3375,7 @@ mod tests {
init_testperson_w_password(idms, TEST_PASSWORD)
.await
.expect("Failed to setup admin account");
let token = check_testperson_password(idms, TEST_PASSWORD).await;
let uat_unverified = check_testperson_password(idms, TEST_PASSWORD, ct).await;
// Process the session info.
let da = idms_delayed.try_recv().expect("invalid");
@ -3616,10 +3383,22 @@ mod tests {
let r = idms.delayed_action(ct, da).await;
assert!(Ok(true) == r);
let uat_unverified = JwsCompact::from_str(&token).expect("Failed to parse apitoken");
let mut idms_prox_read = idms.proxy_read().await;
let jws_validator =
JwsEs256Verifier::try_from(uat_unverified.get_jwk_pubkey().unwrap()).unwrap();
let token_kid = uat_unverified.kid().expect("no key id present");
let uat_jwk = idms_prox_read
.qs_read
.get_key_providers()
.get_key_object(UUID_DOMAIN_INFO)
.and_then(|object| {
object
.jws_public_jwk(&token_kid)
.expect("Unable to access uat jwk")
})
.expect("No jwk by this kid");
let jws_validator = JwsEs256Verifier::try_from(&uat_jwk).unwrap();
let uat_inner: UserAuthToken = jws_validator
.verify(&uat_unverified)
@ -3627,16 +3406,14 @@ mod tests {
.from_json()
.unwrap();
let mut idms_prox_read = idms.proxy_read().await;
// Check it's valid.
idms_prox_read
.validate_client_auth_info_to_ident(token.as_str().into(), ct)
.validate_client_auth_info_to_ident(uat_unverified.clone().into(), ct)
.expect("Failed to validate");
// If the auth session record wasn't processed, this will fail.
idms_prox_read
.validate_client_auth_info_to_ident(token.as_str().into(), post_grace)
.validate_client_auth_info_to_ident(uat_unverified.clone().into(), post_grace)
.expect("Failed to validate");
drop(idms_prox_read);
@ -3652,7 +3429,9 @@ mod tests {
// Now, within gracewindow, it's NOT valid because the session entry exists and is in
// the revoked state!
match idms_prox_read.validate_client_auth_info_to_ident(token.as_str().into(), post_grace) {
match idms_prox_read
.validate_client_auth_info_to_ident(uat_unverified.clone().into(), post_grace)
{
Err(OperationError::SessionExpired) => {}
_ => assert!(false),
}
@ -3677,11 +3456,13 @@ mod tests {
let mut idms_prox_read = idms.proxy_read().await;
idms_prox_read
.validate_client_auth_info_to_ident(token.as_str().into(), ct)
.validate_client_auth_info_to_ident(uat_unverified.clone().into(), ct)
.expect("Failed to validate");
// post grace, it's not valid.
match idms_prox_read.validate_client_auth_info_to_ident(token.as_str().into(), post_grace) {
match idms_prox_read
.validate_client_auth_info_to_ident(uat_unverified.clone().into(), post_grace)
{
Err(OperationError::SessionExpired) => {}
_ => assert!(false),
}
@ -4013,12 +3794,12 @@ mod tests {
idms: &IdmServer,
idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let ct = duration_from_epoch_now();
init_testperson_w_password(idms, TEST_PASSWORD)
.await
.expect("Failed to setup admin account");
let token = check_testperson_password(idms, TEST_PASSWORD).await;
let token = check_testperson_password(idms, TEST_PASSWORD, ct).await;
// Clear the session record
let da = idms_delayed.try_recv().expect("invalid");
@ -4029,29 +3810,27 @@ mod tests {
// Check it's valid.
idms_prox_read
.validate_client_auth_info_to_ident(token.as_str().into(), ct)
.validate_client_auth_info_to_ident(token.clone().into(), ct)
.expect("Failed to validate");
drop(idms_prox_read);
// Now reset the token_key - we can cheat and push this
// through the migrate 3 to 4 code.
//
// fernet_private_key_str
// es256_private_key_der
// We need to get the token key id and revoke it.
let revoke_kid = token.kid().expect("token does not contain a key id");
// Now revoke the token_key
let mut idms_prox_write = idms.proxy_write(ct).await;
let me_reset_tokens = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
ModifyList::new_list(vec![
Modify::Purged(Attribute::FernetPrivateKeyStr.into()),
Modify::Purged(Attribute::Es256PrivateKeyDer.into()),
Modify::Purged(Attribute::DomainTokenKey.into()),
]),
ModifyList::new_append(
Attribute::KeyActionRevoke.into(),
Value::HexString(revoke_kid.to_string()),
),
);
assert!(idms_prox_write.qs_write.modify(&me_reset_tokens).is_ok());
assert!(idms_prox_write.commit().is_ok());
// Check the old token is invalid, due to reload.
let new_token = check_testperson_password(idms, TEST_PASSWORD).await;
let new_token = check_testperson_password(idms, TEST_PASSWORD, ct).await;
// Clear the session record
let da = idms_delayed.try_recv().expect("invalid");
@ -4059,12 +3838,15 @@ mod tests {
idms_delayed.check_is_empty_or_panic();
let mut idms_prox_read = idms.proxy_read().await;
// Check the old token is invalid, due to reload.
assert!(idms_prox_read
.validate_client_auth_info_to_ident(token.as_str().into(), ct)
.validate_client_auth_info_to_ident(token.into(), ct)
.is_err());
// A new token will work due to the matching key.
idms_prox_read
.validate_client_auth_info_to_ident(new_token.as_str().into(), ct)
.validate_client_auth_info_to_ident(new_token.into(), ct)
.expect("Failed to validate");
}

View file

@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use std::time::Duration;
use compact_jwt::{Jws, JwsEs256Signer, JwsSigner};
use compact_jwt::{Jws, JwsCompact, JwsEs256Signer, JwsSigner};
use kanidm_proto::internal::ApiToken as ProtoApiToken;
use time::OffsetDateTime;
@ -30,11 +30,11 @@ macro_rules! try_from_entry {
let jws_key = $value
.get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
.cloned()
.ok_or(OperationError::InvalidAccountState(format!(
"Missing attribute: {}",
Attribute::JwsEs256PrivateKey
)))?
.set_sign_option_embed_jwk(true);
.map(|jws_key| {
jws_key
.set_sign_option_embed_jwk(true)
.set_sign_option_legacy_kid(true)
});
let api_tokens = $value
.get_ava_as_apitoken_map(Attribute::ApiTokenSession)
@ -67,7 +67,7 @@ pub struct ServiceAccount {
pub api_tokens: BTreeMap<Uuid, ApiToken>,
pub jws_key: JwsEs256Signer,
pub jws_key: Option<JwsEs256Signer>,
}
impl ServiceAccount {
@ -185,7 +185,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
&mut self,
gte: &GenerateApiTokenEvent,
ct: Duration,
) -> Result<String, OperationError> {
) -> Result<JwsCompact, OperationError> {
let service_account = self
.qs_write
.internal_search_uuid(gte.target)
@ -256,22 +256,30 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// Provide the event to impersonate
&gte.ident,
)
.and_then(|_| {
// The modify succeeded and was allowed, now sign the token for return.
service_account
.jws_key
.sign(&token)
.map(|jws_signed| jws_signed.to_string())
.map_err(|e| {
admin_error!(err = ?e, "Unable to sign api token");
.map_err(|err| {
error!(?err, "Failed to generate api token");
err
})?;
if self.qs_write.get_domain_version() < DOMAIN_LEVEL_6 {
service_account
.jws_key
.as_ref()
.ok_or_else(|| {
admin_error!("Unable to sign sync token, no sync keys available");
OperationError::CryptographyError
})
.and_then(|jws_key| {
jws_key.sign(&token).map_err(|err| {
admin_error!(?err, "Unable to sign sync token");
OperationError::CryptographyError
})
})
.map_err(|e| {
admin_error!("Failed to generate api token {:?}", e);
e
})
// Done!
})
} else {
self.qs_write
.get_domain_key_object_handle()?
.jws_es256_sign(&token, ct)
}
}
pub fn service_account_destroy_api_token(
@ -413,16 +421,17 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
#[cfg(test)]
mod tests {
use std::str::FromStr;
use std::time::Duration;
use compact_jwt::{JwsCompact, JwsEs256Verifier, JwsVerifier};
use compact_jwt::traits::JwsVerifiable;
use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
use kanidm_proto::internal::ApiToken;
use super::{DestroyApiTokenEvent, GenerateApiTokenEvent};
use crate::event::CreateEvent;
use crate::idm::server::IdmServerTransaction;
use crate::prelude::*;
use crate::server::keys::KeyProvidersTransaction;
use crate::value::KeyStatus;
const TEST_CURRENT_TIME: u64 = 6000;
@ -449,9 +458,10 @@ mod tests {
(Attribute::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());
idms_prox_write
.qs_write
.internal_create(vec![e1])
.expect("Failed to create service account");
let gte = GenerateApiTokenEvent::new_internal(testaccount_uuid, "TestToken", Some(exp));
@ -462,20 +472,16 @@ mod tests {
trace!(?api_token);
// Deserialise it.
let apitoken_unverified =
JwsCompact::from_str(&api_token).expect("Failed to parse apitoken");
let jws_verifier =
JwsEs256Verifier::try_from(apitoken_unverified.get_jwk_pubkey().unwrap()).unwrap();
let jws_verifier = JwsDangerReleaseWithoutVerify::default();
let apitoken_inner = jws_verifier
.verify(&apitoken_unverified)
.verify(&api_token)
.unwrap()
.from_json::<ApiToken>()
.unwrap();
let ident = idms_prox_write
.validate_client_auth_info_to_ident(api_token.as_str().into(), ct)
.validate_client_auth_info_to_ident(api_token.clone().into(), ct)
.expect("Unable to verify api token.");
assert!(ident.get_uuid() == Some(testaccount_uuid));
@ -485,7 +491,7 @@ mod tests {
// Check the expiry
assert!(
idms_prox_write
.validate_client_auth_info_to_ident(api_token.as_str().into(), post_exp)
.validate_client_auth_info_to_ident(api_token.clone().into(), post_exp)
.expect_err("Should not succeed")
== OperationError::SessionExpired
);
@ -500,18 +506,126 @@ mod tests {
// Within gracewindow?
// This is okay, because we are within the gracewindow.
let ident = idms_prox_write
.validate_client_auth_info_to_ident(api_token.as_str().into(), ct)
.validate_client_auth_info_to_ident(api_token.clone().into(), ct)
.expect("Unable to verify api token.");
assert!(ident.get_uuid() == Some(testaccount_uuid));
// Past gracewindow?
assert!(
idms_prox_write
.validate_client_auth_info_to_ident(api_token.as_str().into(), past_grc)
.validate_client_auth_info_to_ident(api_token.into(), past_grc)
.expect_err("Should not succeed")
== OperationError::SessionExpired
);
assert!(idms_prox_write.commit().is_ok());
}
#[idm_test(domain_level=DOMAIN_LEVEL_5)]
async fn test_idm_service_account_dl5_dl6_api_token(
idms: &IdmServer,
_idms_delayed: &mut IdmServerDelayed,
) {
let ct = Duration::from_secs(TEST_CURRENT_TIME);
let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000);
let mut idms_prox_write = idms.proxy_write(ct).await;
assert_eq!(
idms_prox_write.qs_write.get_domain_version(),
DOMAIN_LEVEL_5
);
let testaccount_uuid = Uuid::new_v4();
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Name, Value::new_iname("test_account_only")),
(Attribute::Uuid, Value::Uuid(testaccount_uuid)),
(Attribute::Description, Value::new_utf8s("testaccount")),
(Attribute::DisplayName, Value::new_utf8s("testaccount"))
);
idms_prox_write
.qs_write
.internal_create(vec![e1])
.expect("Failed to create service account");
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);
assert!(idms_prox_write.commit().is_ok());
// Now trigger 5 -> 6
let mut idms_prox_write = idms.proxy_write(ct).await;
idms_prox_write
.qs_write
.internal_apply_domain_migration(DOMAIN_LEVEL_6)
.expect("Unable to set domain level to version 6");
assert!(idms_prox_write.commit().is_ok());
// Now check our api token still validates.
let mut idms_prox_write = idms.proxy_write(ct).await;
// Check a new token is domain key signed.
let gte = GenerateApiTokenEvent::new_internal(testaccount_uuid, "TestToken", Some(exp));
let new_api_token = idms_prox_write
.service_account_generate_api_token(&gte, ct)
.expect("failed to generate new api token");
assert_ne!(api_token.kid(), new_api_token.kid());
// Check that both tokens verify and work.
let _ident = idms_prox_write
.validate_client_auth_info_to_ident(api_token.clone().into(), ct)
.expect("Unable to verify old api token.");
let _ident = idms_prox_write
.validate_client_auth_info_to_ident(new_api_token.clone().into(), ct)
.expect("Unable to verify new api token.");
// The former key is now on the domain object.
let key_object = idms_prox_write
.qs_write
.get_key_providers()
.get_key_object(UUID_DOMAIN_INFO)
.expect("Unable to retrieve key object by uuid");
// Assert the former key is now in the domain key object, and now is "retained".
let former_kid = api_token.kid().unwrap().to_string();
let status = key_object
.kid_status(&former_kid)
.expect("Failed to access kid status");
assert_eq!(status, Some(KeyStatus::Retained));
assert!(idms_prox_write.commit().is_ok());
// Now trigger 6 -> 7
let mut idms_prox_write = idms.proxy_write(ct).await;
idms_prox_write
.qs_write
.internal_apply_domain_migration(DOMAIN_LEVEL_7)
.expect("Unable to set domain level to version 7");
assert!(idms_prox_write.commit().is_ok());
// The key on the service account is removed.
let mut idms_prox_write = idms.proxy_write(ct).await;
let service_entry = idms_prox_write
.qs_write
.internal_search_uuid(testaccount_uuid)
.expect("Unable to access service account");
assert!(!service_entry.attribute_pres(Attribute::JwsEs256PrivateKey));
assert!(idms_prox_write.commit().is_ok());
}
}

View file

@ -118,14 +118,14 @@ impl Domain {
e.set_ava(Attribute::DomainDisplayName, once(domain_display_name));
}
if !e.attribute_pres(Attribute::FernetPrivateKeyStr) {
if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::FernetPrivateKeyStr) {
security_info!("regenerating domain token encryption key");
let k = fernet::Fernet::generate_key();
let v = Value::new_secret_str(&k);
e.add_ava(Attribute::FernetPrivateKeyStr, v);
}
if !e.attribute_pres(Attribute::Es256PrivateKeyDer) {
if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::Es256PrivateKeyDer) {
security_info!("regenerating domain es256 private key");
let der = JwsEs256Signer::generate_es256()
.and_then(|jws| jws.private_key_to_der())
@ -137,7 +137,7 @@ impl Domain {
e.add_ava(Attribute::Es256PrivateKeyDer, v);
}
if !e.attribute_pres(Attribute::PrivateCookieKey) {
if qs.get_domain_version() < DOMAIN_LEVEL_6 && !e.attribute_pres(Attribute::PrivateCookieKey) {
security_info!("regenerating domain cookie key");
e.add_ava(Attribute::PrivateCookieKey, generate_domain_cookie_key());
}

View file

@ -15,36 +15,39 @@ impl Plugin for JwsKeygen {
#[instrument(level = "debug", name = "jwskeygen_pre_create_transform", skip_all)]
fn pre_create_transform(
_qs: &mut QueryServerWriteTransaction,
qs: &mut QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
_ce: &CreateEvent,
) -> Result<(), OperationError> {
Self::modify_inner(cand)
Self::modify_inner(qs, cand)
}
#[instrument(level = "debug", name = "jwskeygen_pre_modify", skip_all)]
fn pre_modify(
_qs: &mut QueryServerWriteTransaction,
qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &ModifyEvent,
) -> Result<(), OperationError> {
Self::modify_inner(cand)
Self::modify_inner(qs, cand)
}
#[instrument(level = "debug", name = "jwskeygen_pre_batch_modify", skip_all)]
fn pre_batch_modify(
_qs: &mut QueryServerWriteTransaction,
qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &BatchModifyEvent,
) -> Result<(), OperationError> {
Self::modify_inner(cand)
Self::modify_inner(qs, cand)
}
}
impl JwsKeygen {
fn modify_inner<T: Clone>(cand: &mut [Entry<EntryInvalid, T>]) -> Result<(), OperationError> {
fn modify_inner<T: Clone>(
qs: &mut QueryServerWriteTransaction,
cand: &mut [Entry<EntryInvalid, T>],
) -> Result<(), OperationError> {
cand.iter_mut().try_for_each(|e| {
if e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServerBasic.into()) &&
!e.attribute_pres(Attribute::OAuth2RsBasicSecret) {
@ -85,7 +88,8 @@ impl JwsKeygen {
}
}
if (e.attribute_equality(Attribute::Class, &EntryClass::ServiceAccount.into()) ||
if qs.get_domain_version() < DOMAIN_LEVEL_6 &&
(e.attribute_equality(Attribute::Class, &EntryClass::ServiceAccount.into()) ||
e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into())) &&
!e.attribute_pres(Attribute::JwsEs256PrivateKey) {
security_info!("regenerating jws es256 private key");

View file

@ -0,0 +1,232 @@
use crate::plugins::Plugin;
use crate::prelude::*;
use std::sync::Arc;
pub struct KeyObjectManagement {}
impl Plugin for KeyObjectManagement {
fn id() -> &'static str {
"plugin_keyobject_management"
}
#[instrument(
level = "debug",
name = "keyobject_management::pre_create_transform",
skip_all
)]
fn pre_create_transform(
qs: &mut QueryServerWriteTransaction,
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
_ce: &CreateEvent,
) -> Result<(), OperationError> {
Self::apply_keyobject_inner(qs, cand)
}
#[instrument(level = "debug", name = "keyobject_management::pre_modify", skip_all)]
fn pre_modify(
qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &ModifyEvent,
) -> Result<(), OperationError> {
Self::apply_keyobject_inner(qs, cand)
}
#[instrument(
level = "debug",
name = "keyobject_management::pre_batch_modify",
skip_all
)]
fn pre_batch_modify(
qs: &mut QueryServerWriteTransaction,
_pre_cand: &[Arc<EntrySealedCommitted>],
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_me: &BatchModifyEvent,
) -> Result<(), OperationError> {
Self::apply_keyobject_inner(qs, cand)
}
/*
#[instrument(level = "debug", name = "keyobject_management::pre_delete", skip_all)]
fn pre_delete(
_qs: &mut QueryServerWriteTransaction,
// Should these be EntrySealed
_cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
_de: &DeleteEvent,
) -> Result<(), OperationError> {
Ok(())
}
*/
#[instrument(level = "debug", name = "keyobject_management::verify", skip_all)]
fn verify(qs: &mut QueryServerReadTransaction) -> Vec<Result<(), ConsistencyError>> {
let filt_in = filter!(f_eq(Attribute::Class, EntryClass::KeyProvider.into()));
let key_providers = match qs
.internal_search(filt_in)
.map_err(|_| Err(ConsistencyError::QueryServerSearchFailure))
{
Ok(all_cand) => all_cand,
Err(e) => return vec![e],
};
// Put the providers into a map by uuid.
let key_providers: hashbrown::HashSet<_> = key_providers
.into_iter()
.map(|entry| entry.get_uuid())
.collect();
let filt_in = filter!(f_eq(Attribute::Class, EntryClass::KeyObject.into()));
let key_objects = match qs
.internal_search(filt_in)
.map_err(|_| Err(ConsistencyError::QueryServerSearchFailure))
{
Ok(all_cand) => all_cand,
Err(e) => return vec![e],
};
let errs = key_objects
.into_iter()
.filter_map(|key_object_entry| {
let object_uuid = key_object_entry.get_uuid();
// Each key objects must relate to a provider.
let Some(provider_uuid) =
key_object_entry.get_ava_single_refer(Attribute::KeyProvider)
else {
error!(?object_uuid, "Invalid key object, no key provider uuid.");
return Some(ConsistencyError::KeyProviderUuidMissing {
key_object: object_uuid,
});
};
if !key_providers.contains(&provider_uuid) {
error!(
?object_uuid,
?provider_uuid,
"Invalid key object, key provider referenced is not found."
);
return Some(ConsistencyError::KeyProviderNotFound {
key_object: object_uuid,
provider: provider_uuid,
});
}
// Every key object needs at least *one* key it stores.
if !key_object_entry
.attribute_equality(Attribute::Class, &EntryClass::KeyObjectJwtEs256.into())
{
error!(?object_uuid, "Invalid key object, contains no keys.");
return Some(ConsistencyError::KeyProviderNoKeys {
key_object: object_uuid,
});
}
None
})
.map(|err| Err(err))
.collect::<Vec<_>>();
errs
}
}
impl KeyObjectManagement {
fn apply_keyobject_inner<T: Clone>(
qs: &mut QueryServerWriteTransaction,
cand: &mut [Entry<EntryInvalid, T>],
) -> Result<(), OperationError> {
// Valid from right meow!
let valid_from = qs.get_curtime();
let txn_cid = qs.get_cid().clone();
let key_providers = qs.get_key_providers_mut();
cand.iter_mut()
.filter(|entry| {
entry.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
})
.try_for_each(|entry| {
// The entry should not have set any type of KeyObject at this point.
// Should we force delete those attrs here just incase?
entry.remove_ava(Attribute::Class, &EntryClass::KeyObjectInternal.into());
// Must be set by now.
let key_object_uuid = entry
.get_uuid()
.ok_or(OperationError::KP0008KeyObjectMissingUuid)?;
trace!(?key_object_uuid, "Setting up key object");
// Get the default provider, and create a new ephemeral key object
// inside it. If the object existed already, we clone it so that we can stage
// our changes.
let mut key_object = key_providers.get_or_create_in_default(key_object_uuid)?;
// Import any keys that we were asked to import. This is before revocation so that
// any keyId here might also be able to be revoked.
let maybe_import = entry.pop_ava(Attribute::KeyActionImportJwsEs256);
if let Some(import_keys) = maybe_import
.as_ref()
.and_then(|vs| vs.as_private_binary_set())
{
key_object.jws_es256_import(import_keys, valid_from, &txn_cid)?;
}
// If revoke. This weird looking let dance is to ensure that the inner hexstring set
// lives long enough.
let maybe_revoked = entry.pop_ava(Attribute::KeyActionRevoke);
if let Some(revoke_keys) =
maybe_revoked.as_ref().and_then(|vs| vs.as_hexstring_set())
{
key_object.revoke_keys(revoke_keys, &txn_cid)?;
}
// Rotation is after revocation, but before assertion. This way if the user
// asked for rotation and revocation, we don't double rotate when we get to
// the assert phase. We also only get a rotation time if the time is in the
// future, to avoid rotating keys in the past.
if let Some(rotation_time) = entry
.pop_ava(Attribute::KeyActionRotate)
.and_then(|vs| vs.to_datetime_single())
.and_then(|odt| {
let secs = odt.unix_timestamp() as u64;
if secs > valid_from.as_secs() {
Some(Duration::from_secs(secs))
} else {
None
}
})
{
key_object.rotate_keys(rotation_time, &txn_cid)?;
}
if entry.attribute_equality(Attribute::Class, &EntryClass::KeyObjectJwtEs256.into())
{
// Assert that this object has a valid es256 key present. Post revoke, it may NOT
// be present. This differs to rotate, in that the assert verifes we have at least
// *one* key that is valid in all conditions.
key_object.jws_es256_assert(Duration::ZERO, &txn_cid)?;
}
if entry
.attribute_equality(Attribute::Class, &EntryClass::KeyObjectJweA128GCM.into())
{
key_object.jwe_a128gcm_assert(Duration::ZERO, &txn_cid)?;
}
// Turn that object into it's entry template to create. I think we need to make this
// some kind of merge_vs?
key_object.into_valuesets()?.into_iter().try_for_each(
|(attribute, valueset)| entry.merge_ava_set(attribute, valueset),
)?;
Ok(())
})
}
}
// Unlike other plugins, tests for this plugin will be located in server/lib/src/server/keys.
//
// The reason is because we can preconfigure different providers to test these paths in future.

View file

@ -96,6 +96,7 @@ fn do_memberof(
Ok(())
}
// This is how you know the good code is here.
#[allow(clippy::cognitive_complexity)]
fn apply_memberof(
qs: &mut QueryServerWriteTransaction,

View file

@ -19,6 +19,7 @@ pub(crate) mod dyngroup;
mod eckeygen;
pub(crate) mod gidnumber;
mod jwskeygen;
mod keyobject;
mod memberof;
mod namehistory;
mod protected;
@ -230,6 +231,7 @@ impl Plugins {
base::Base::pre_create_transform(qs, cand, ce)?;
valuedeny::ValueDeny::pre_create_transform(qs, cand, ce)?;
cred_import::CredImport::pre_create_transform(qs, cand, ce)?;
keyobject::KeyObjectManagement::pre_create_transform(qs, cand, ce)?;
jwskeygen::JwsKeygen::pre_create_transform(qs, cand, ce)?;
gidnumber::GidNumber::pre_create_transform(qs, cand, ce)?;
domain::Domain::pre_create_transform(qs, cand, ce)?;
@ -272,6 +274,7 @@ impl Plugins {
valuedeny::ValueDeny::pre_modify(qs, pre_cand, cand, me)?;
cred_import::CredImport::pre_modify(qs, pre_cand, cand, me)?;
jwskeygen::JwsKeygen::pre_modify(qs, pre_cand, cand, me)?;
keyobject::KeyObjectManagement::pre_modify(qs, pre_cand, cand, me)?;
gidnumber::GidNumber::pre_modify(qs, pre_cand, cand, me)?;
domain::Domain::pre_modify(qs, pre_cand, cand, me)?;
spn::Spn::pre_modify(qs, pre_cand, cand, me)?;
@ -307,6 +310,7 @@ impl Plugins {
valuedeny::ValueDeny::pre_batch_modify(qs, pre_cand, cand, me)?;
cred_import::CredImport::pre_batch_modify(qs, pre_cand, cand, me)?;
jwskeygen::JwsKeygen::pre_batch_modify(qs, pre_cand, cand, me)?;
keyobject::KeyObjectManagement::pre_batch_modify(qs, pre_cand, cand, me)?;
gidnumber::GidNumber::pre_batch_modify(qs, pre_cand, cand, me)?;
domain::Domain::pre_batch_modify(qs, pre_cand, cand, me)?;
spn::Spn::pre_batch_modify(qs, pre_cand, cand, me)?;
@ -418,6 +422,7 @@ impl Plugins {
run_verify_plugin!(qs, results, valuedeny::ValueDeny);
run_verify_plugin!(qs, results, attrunique::AttrUnique);
run_verify_plugin!(qs, results, refint::ReferentialIntegrity);
run_verify_plugin!(qs, results, keyobject::KeyObjectManagement);
run_verify_plugin!(qs, results, dyngroup::DynGroup);
run_verify_plugin!(qs, results, memberof::MemberOf);
run_verify_plugin!(qs, results, spn::Spn);

View file

@ -28,6 +28,8 @@ lazy_static! {
m.insert(Attribute::DomainLdapBasedn);
m.insert(Attribute::FernetPrivateKeyStr);
m.insert(Attribute::Es256PrivateKeyDer);
m.insert(Attribute::KeyActionRevoke);
m.insert(Attribute::KeyActionRotate);
m.insert(Attribute::IdVerificationEcKey);
m.insert(Attribute::BadlistPassword);
m.insert(Attribute::DeniedName);

View file

@ -1,6 +1,7 @@
use super::proto::*;
use crate::plugins::Plugins;
use crate::prelude::*;
use crate::server::ChangeFlag;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
@ -219,35 +220,51 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.changed_uuid.extend(cand.iter().map(|e| e.get_uuid()));
if !self.changed_acp {
self.changed_acp = cand
if !self.changed_flags.contains(ChangeFlag::ACP)
&& cand
.iter()
.chain(pre_cand.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
})
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_oauth2 {
self.changed_oauth2 = cand
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& cand
.iter()
.chain(pre_cand.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
});
})
{
self.changed_flags.insert(ChangeFlag::OAUTH2)
}
if !self.changed_sync_agreement {
self.changed_sync_agreement = cand
if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
&& cand
.iter()
.chain(pre_cand.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()));
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
{
self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
}
if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
&& cand
.iter()
.chain(pre_cand.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
})
{
self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
}
trace!(
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
changed_sync_agreement = ?self.changed_sync_agreement
changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
);
Ok(true)
@ -371,6 +388,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
error!("Failed to reload domain info");
e
})?;
self.reload_system_config().map_err(|e| {
error!("Failed to reload system configuration");
e
})?;
}
debug!("Applying all context entries");
@ -579,10 +600,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
})?;
// Mark that everything changed so that post commit hooks function as expected.
self.changed_schema = true;
self.changed_acp = true;
self.changed_oauth2 = true;
self.changed_domain = true;
self.changed_flags.insert(
ChangeFlag::SCHEMA
| ChangeFlag::ACP
| ChangeFlag::OAUTH2
| ChangeFlag::DOMAIN
| ChangeFlag::SYSTEM_CONFIG
| ChangeFlag::SYNC_AGREEMENT
| ChangeFlag::KEY_MATERIAL,
);
// That's it! We are GOOD to go!

View file

@ -2,6 +2,7 @@ use super::cid::Cid;
use super::entry::EntryChangeState;
use super::entry::State;
use crate::be::dbvalue::DbValueImage;
use crate::be::dbvalue::DbValueKeyInternal;
use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
use crate::entry::Eattrs;
use crate::prelude::*;
@ -452,6 +453,12 @@ pub enum ReplAttrV1 {
WebauthnAttestationCaList {
ca_list: AttestationCaList,
},
KeyInternal {
set: Vec<DbValueKeyInternal>,
},
HexString {
set: Vec<String>,
},
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]

View file

@ -206,11 +206,12 @@ impl<'a> QueryServerReadTransaction<'a> {
});
let (meta_entries, entries): (Vec<_>, Vec<_>) = rem_entries.into_iter().partition(|e| {
e.get_ava_set(Attribute::Uuid)
.map(|uset| {
uset.contains(&PVUUID_DOMAIN_INFO as &PartialValue)
|| uset.contains(&PVUUID_SYSTEM_INFO as &PartialValue)
|| uset.contains(&PVUUID_SYSTEM_CONFIG as &PartialValue)
e.get_ava_set(Attribute::Class)
.map(|cls| {
cls.contains(&EntryClass::DomainInfo.into() as &PartialValue)
|| cls.contains(&EntryClass::SystemInfo.into() as &PartialValue)
|| cls.contains(&EntryClass::SystemConfig.into() as &PartialValue)
|| cls.contains(&EntryClass::KeyProvider.into() as &PartialValue)
})
.unwrap_or(false)
});
@ -285,28 +286,26 @@ impl<'a> QueryServerReadTransaction<'a> {
// - We must exclude certain entries and attributes!
// * schema defines what we exclude!
let schema_filter = filter!(f_or!([
let schema_filter_inner = f_or!([
f_eq(Attribute::Class, EntryClass::AttributeType.into()),
f_eq(Attribute::Class, EntryClass::ClassType.into()),
]));
]);
let meta_filter = filter!(f_or!([
f_eq(Attribute::Uuid, PVUUID_DOMAIN_INFO.clone()),
f_eq(Attribute::Uuid, PVUUID_SYSTEM_INFO.clone()),
f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone()),
]));
let schema_filter = filter!(schema_filter_inner.clone());
let meta_filter_inner = f_or!([
f_eq(Attribute::Class, EntryClass::DomainInfo.into()),
f_eq(Attribute::Class, EntryClass::SystemInfo.into()),
f_eq(Attribute::Class, EntryClass::SystemConfig.into()),
f_eq(Attribute::Class, EntryClass::KeyProvider.into()),
]);
let meta_filter = filter!(meta_filter_inner.clone());
let entry_filter = filter_all!(f_or!([
f_and!([
f_pres(Attribute::Class),
f_andnot(f_or(vec![
// These are from above!
f_eq(Attribute::Class, EntryClass::AttributeType.into()),
f_eq(Attribute::Class, EntryClass::ClassType.into()),
f_eq(Attribute::Uuid, PVUUID_DOMAIN_INFO.clone()),
f_eq(Attribute::Uuid, PVUUID_SYSTEM_INFO.clone()),
f_eq(Attribute::Uuid, PVUUID_SYSTEM_CONFIG.clone()),
])),
f_andnot(f_or(vec![schema_filter_inner, meta_filter_inner])),
]),
f_eq(Attribute::Class, EntryClass::Tombstone.into()),
f_eq(Attribute::Class, EntryClass::Recycled.into()),

View file

@ -236,6 +236,10 @@ impl SchemaAttribute {
SyntaxType::AuditLogString => matches!(v, PartialValue::Utf8(_)),
SyntaxType::Image => matches!(v, PartialValue::Utf8(_)),
SyntaxType::CredentialType => matches!(v, PartialValue::CredentialType(_)),
SyntaxType::HexString => matches!(v, PartialValue::HexString(_)),
SyntaxType::KeyInternal => matches!(v, PartialValue::HexString(_)),
SyntaxType::WebauthnAttestationCaList => false,
};
if r {
@ -297,6 +301,8 @@ impl SchemaAttribute {
SyntaxType::WebauthnAttestationCaList => {
matches!(v, Value::WebauthnAttestationCaList(_))
}
SyntaxType::KeyInternal => matches!(v, Value::KeyInternal { .. }),
SyntaxType::HexString => matches!(v, Value::HexString(_)),
};
if r {
Ok(())

View file

@ -21,7 +21,6 @@ use std::sync::Arc;
use concread::arcache::{ARCache, ARCacheBuilder, ARCacheReadTxn};
use concread::cowcell::*;
use tracing::trace;
use uuid::Uuid;
use crate::entry::{Entry, EntryCommitted, EntryInit, EntryNew, EntryReduced};

View file

@ -1,4 +1,4 @@
use super::QueryServerWriteTransaction;
use super::{ChangeFlag, QueryServerWriteTransaction};
use crate::prelude::*;
use crate::server::Plugins;
use hashbrown::HashMap;
@ -184,36 +184,77 @@ impl<'a> QueryServerWriteTransaction<'a> {
// We have finished all plugs and now have a successful operation - flag if
// schema or acp requires reload. Remember, this is a modify, so we need to check
// pre and post cands.
if !self.changed_schema {
self.changed_schema = norm_cand
if !self.changed_flags.contains(ChangeFlag::SCHEMA)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
});
})
{
self.changed_flags.insert(ChangeFlag::SCHEMA)
}
if !self.changed_acp {
self.changed_acp = norm_cand
if !self.changed_flags.contains(ChangeFlag::ACP)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
});
})
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_oauth2 {
self.changed_oauth2 = norm_cand
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
});
})
{
self.changed_flags.insert(ChangeFlag::OAUTH2)
}
if !self.changed_domain {
self.changed_domain = norm_cand
if !self.changed_flags.contains(ChangeFlag::DOMAIN)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
{
self.changed_flags.insert(ChangeFlag::DOMAIN)
}
if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
{
self.changed_flags.insert(ChangeFlag::SYSTEM_CONFIG)
}
if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
{
self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
}
if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
})
{
self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
}
self.changed_uuid.extend(
@ -224,10 +265,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
);
trace!(
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
);
// return

View file

@ -1,6 +1,6 @@
use crate::prelude::*;
use crate::server::CreateEvent;
use crate::server::Plugins;
use crate::server::{ChangeFlag, Plugins};
impl<'a> QueryServerWriteTransaction<'a> {
#[instrument(level = "debug", skip_all)]
@ -54,19 +54,12 @@ impl<'a> QueryServerWriteTransaction<'a> {
// run any pre plugins, giving them the list of mutable candidates.
// pre-plugins are defined here in their correct order of calling!
// I have no intent to make these dynamic or configurable.
Plugins::run_pre_create_transform(self, &mut candidates, ce).map_err(|e| {
admin_error!("Create operation failed (pre_transform plugin), {:?}", e);
e
})?;
// NOTE: This is how you map from Vec<Result<T>> to Result<Vec<T>>
// remember, that you only get the first error and the iter terminates.
// eprintln!("{:?}", candidates);
// Now, normalise AND validate!
let norm_cand = candidates
.into_iter()
.map(|e| {
@ -83,7 +76,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
.collect::<Result<Vec<EntrySealedNew>, _>>()?;
// Run any pre-create plugins now with schema validated entries.
// This is important for normalisation of certain types IE class
// This is important for normalisation of certain types i.e. class
// or attributes for these checks.
Plugins::run_pre_create(self, &norm_cand, ce).map_err(|e| {
admin_error!("Create operation failed (plugin), {:?}", e);
@ -97,55 +90,72 @@ impl<'a> QueryServerWriteTransaction<'a> {
})?;
// Run any post plugins
Plugins::run_post_create(self, &commit_cand, ce).map_err(|e| {
admin_error!("Create operation failed (post plugin), {:?}", e);
e
})?;
// We have finished all plugs and now have a successful operation - flag if
// We have finished all plugins and now have a successful operation - flag if
// schema or acp requires reload.
if !self.changed_schema {
self.changed_schema = commit_cand.iter().any(|e| {
if !self.changed_flags.contains(ChangeFlag::SCHEMA)
&& commit_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
});
})
{
self.changed_flags.insert(ChangeFlag::SCHEMA)
}
if !self.changed_acp {
self.changed_acp = commit_cand.iter().any(|e| {
if !self.changed_flags.contains(ChangeFlag::ACP)
&& commit_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
});
})
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_oauth2 {
self.changed_oauth2 = commit_cand.iter().any(|e| {
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& commit_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
});
})
{
self.changed_flags.insert(ChangeFlag::OAUTH2)
}
if !self.changed_domain {
self.changed_domain = commit_cand
if !self.changed_flags.contains(ChangeFlag::DOMAIN)
&& commit_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
{
self.changed_flags.insert(ChangeFlag::DOMAIN)
}
if !self.changed_system_config {
self.changed_system_config = commit_cand
if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
&& commit_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
{
self.changed_flags.insert(ChangeFlag::SYSTEM_CONFIG)
}
if !self.changed_sync_agreement {
self.changed_sync_agreement = commit_cand
if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
&& commit_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()));
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
{
self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
}
if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
&& commit_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
})
{
self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
}
self.changed_uuid
.extend(commit_cand.iter().map(|e| e.get_uuid()));
trace!(
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
system_config_reload = ?self.changed_system_config,
changed_sync_agreement = ?self.changed_sync_agreement,
changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
);
// We are complete, finalise logging and return

View file

@ -1,6 +1,6 @@
use crate::plugins::Plugins;
use crate::prelude::*;
use crate::server::DeleteEvent;
use crate::server::{ChangeFlag, Plugins};
impl<'a> QueryServerWriteTransaction<'a> {
#[allow(clippy::cognitive_complexity)]
@ -100,48 +100,63 @@ impl<'a> QueryServerWriteTransaction<'a> {
// We have finished all plugs and now have a successful operation - flag if
// schema or acp requires reload.
if !self.changed_schema {
self.changed_schema = del_cand.iter().any(|e| {
if !self.changed_flags.contains(ChangeFlag::SCHEMA)
&& del_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
});
})
{
self.changed_flags.insert(ChangeFlag::SCHEMA)
}
if !self.changed_acp {
self.changed_acp = del_cand.iter().any(|e| {
if !self.changed_flags.contains(ChangeFlag::ACP)
&& del_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
});
})
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_oauth2 {
self.changed_oauth2 = del_cand.iter().any(|e| {
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& del_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
});
})
{
self.changed_flags.insert(ChangeFlag::OAUTH2)
}
if !self.changed_domain {
self.changed_domain = del_cand
if !self.changed_flags.contains(ChangeFlag::DOMAIN)
&& del_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
{
self.changed_flags.insert(ChangeFlag::DOMAIN)
}
if !self.changed_system_config {
self.changed_system_config = del_cand
if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
&& del_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
{
self.changed_flags.insert(ChangeFlag::SYSTEM_CONFIG)
}
if !self.changed_sync_agreement {
self.changed_sync_agreement = del_cand
if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
&& del_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Uuid, &EntryClass::SyncAccount.into()));
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
{
self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
}
if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
&& del_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
})
{
self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
}
self.changed_uuid
.extend(del_cand.iter().map(|e| e.get_uuid()));
trace!(
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
system_config_reload = ?self.changed_system_config,
changed_sync_agreement = ?self.changed_sync_agreement
changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
);
// Send result

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
mod internal;
mod object;
mod provider;
pub type KeyId = String;
#[cfg(test)]
pub(crate) use self::internal::KeyObjectInternal;
pub(crate) use self::object::KeyObject;
pub(crate) use self::provider::{
KeyProvider, KeyProviders, KeyProvidersReadTransaction, KeyProvidersTransaction,
KeyProvidersWriteTransaction,
};

View file

@ -0,0 +1,61 @@
use crate::prelude::*;
use compact_jwt::{compact::JweCompact, jwe::Jwe};
use compact_jwt::{Jwk, Jws, JwsCompact};
use smolset::SmolSet;
use std::collections::BTreeSet;
use uuid::Uuid;
pub type KeyObject = Box<dyn KeyObjectT + Send + Sync + 'static>;
pub type KeyObjectRef<'a> = &'a (dyn KeyObjectT + Send + Sync + 'static);
pub trait KeyObjectT {
fn uuid(&self) -> Uuid;
fn jws_es256_import(
&mut self,
import_keys: &SmolSet<[Vec<u8>; 1]>,
valid_from: Duration,
cid: &Cid,
) -> Result<(), OperationError>;
fn jws_es256_assert(&mut self, valid_from: Duration, cid: &Cid) -> Result<(), OperationError>;
fn jws_es256_sign(
&self,
jws: &Jws,
current_time: Duration,
) -> Result<JwsCompact, OperationError>;
fn jws_verify(&self, jwsc: &JwsCompact) -> Result<Jws, OperationError>;
fn jws_public_jwk(&self, kid: &str) -> Result<Option<Jwk>, OperationError>;
fn jwe_a128gcm_assert(&mut self, valid_from: Duration, cid: &Cid)
-> Result<(), OperationError>;
fn jwe_a128gcm_encrypt(
&self,
jwe: &Jwe,
current_time: Duration,
) -> Result<JweCompact, OperationError>;
fn jwe_decrypt(&self, jwec: &JweCompact) -> Result<Jwe, OperationError>;
fn into_valuesets(&self) -> Result<Vec<(Attribute, ValueSet)>, OperationError>;
fn duplicate(&self) -> KeyObject;
fn rotate_keys(&mut self, current_time: Duration, cid: &Cid) -> Result<(), OperationError>;
fn revoke_keys(
&mut self,
revoke_set: &BTreeSet<String>,
cid: &Cid,
) -> Result<(), OperationError>;
#[cfg(test)]
fn kid_status(
&self,
kid: &super::KeyId,
) -> Result<Option<crate::value::KeyStatus>, OperationError>;
}

View file

@ -0,0 +1,382 @@
use crate::prelude::*;
use concread::cowcell::*;
use uuid::Uuid;
use std::collections::BTreeMap;
use std::fmt;
use std::ops::Deref;
use std::sync::Arc;
use super::internal::KeyProviderInternal;
use super::object::{KeyObject, KeyObjectRef};
#[derive(Clone)]
pub enum KeyProvider {
// Mostly this is a wrapper to store the loaded providers, which are then downcast into
// their concrete type and associated with key objects.
Internal(Arc<KeyProviderInternal>),
}
impl fmt::Display for KeyProvider {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeyProvider")
.field("name", &self.name())
.field("uuid", &self.uuid())
.finish()
}
}
impl KeyProvider {
pub(crate) fn uuid(&self) -> Uuid {
match self {
KeyProvider::Internal(inner) => inner.uuid(),
}
}
pub(crate) fn name(&self) -> &str {
match self {
KeyProvider::Internal(inner) => inner.name(),
}
}
pub(crate) fn test(&self) -> Result<(), OperationError> {
match self {
KeyProvider::Internal(inner) => inner.test(),
}
}
fn create_new_key_object(&self, key_object_uuid: Uuid) -> Result<KeyObject, OperationError> {
match self {
KeyProvider::Internal(inner) => {
inner.create_new_key_object(key_object_uuid, inner.clone())
}
}
}
fn load_key_object(
&self,
entry: &EntrySealedCommitted,
) -> Result<Arc<KeyObject>, OperationError> {
match self {
KeyProvider::Internal(inner) => inner.load_key_object(entry, inner.clone()),
}
}
pub(crate) fn try_from(
value: &Entry<EntrySealed, EntryCommitted>,
) -> Result<Arc<Self>, OperationError> {
if !value.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into()) {
error!("class key_provider not present.");
return Err(OperationError::KP0002KeyProviderInvalidClass);
}
if value.attribute_equality(Attribute::Class, &EntryClass::KeyProviderInternal.into()) {
KeyProviderInternal::try_from(value)
.map(|kpi| KeyProvider::Internal(Arc::new(kpi)))
.map(Arc::new)
} else {
error!("No supported key provider type present");
Err(OperationError::KP0003KeyProviderInvalidType)
}
}
}
#[derive(Clone)]
struct KeyProvidersInner {
// Wondering if this should be Arc later to allow KeyObjects to refer to their provider directly.
providers: BTreeMap<Uuid, Arc<KeyProvider>>,
objects: BTreeMap<Uuid, Arc<KeyObject>>,
}
pub struct KeyProviders {
inner: CowCell<KeyProvidersInner>,
}
impl Default for KeyProviders {
fn default() -> Self {
KeyProviders {
inner: CowCell::new(KeyProvidersInner {
providers: BTreeMap::default(),
objects: BTreeMap::default(),
}),
}
}
}
impl KeyProviders {
pub fn read(&self) -> KeyProvidersReadTransaction {
KeyProvidersReadTransaction {
inner: self.inner.read(),
}
}
pub fn write(&self) -> KeyProvidersWriteTransaction {
KeyProvidersWriteTransaction {
inner: self.inner.write(),
}
}
}
pub trait KeyProvidersTransaction {
fn get_uuid(&self, key_provider_uuid: Uuid) -> Option<&KeyProvider>;
fn get_key_object(&self, key_object_uuid: Uuid) -> Option<KeyObjectRef>;
fn get_key_object_handle(&self, key_object_uuid: Uuid) -> Option<Arc<KeyObject>>;
}
pub struct KeyProvidersReadTransaction {
inner: CowCellReadTxn<KeyProvidersInner>,
}
impl KeyProvidersTransaction for KeyProvidersReadTransaction {
fn get_uuid(&self, key_provider_uuid: Uuid) -> Option<&KeyProvider> {
self.inner
.deref()
.providers
.get(&key_provider_uuid)
.map(|k| k.as_ref())
}
fn get_key_object(&self, key_object_uuid: Uuid) -> Option<KeyObjectRef> {
self.inner
.deref()
.objects
.get(&key_object_uuid)
.map(|k| k.as_ref().as_ref())
}
fn get_key_object_handle(&self, key_object_uuid: Uuid) -> Option<Arc<KeyObject>> {
self.inner
.deref()
.objects
.get(&key_object_uuid)
.map(|k| k.clone())
}
}
pub struct KeyProvidersWriteTransaction<'a> {
inner: CowCellWriteTxn<'a, KeyProvidersInner>,
}
impl<'a> KeyProvidersTransaction for KeyProvidersWriteTransaction<'a> {
fn get_uuid(&self, key_provider_uuid: Uuid) -> Option<&KeyProvider> {
self.inner
.deref()
.providers
.get(&key_provider_uuid)
.map(|k| k.as_ref())
}
fn get_key_object(&self, key_object_uuid: Uuid) -> Option<KeyObjectRef> {
self.inner
.deref()
.objects
.get(&key_object_uuid)
.map(|k| k.as_ref().as_ref())
}
fn get_key_object_handle(&self, key_object_uuid: Uuid) -> Option<Arc<KeyObject>> {
self.inner
.deref()
.objects
.get(&key_object_uuid)
.map(|k| k.clone())
}
}
impl<'a> KeyProvidersWriteTransaction<'a> {
#[cfg(test)]
pub(crate) fn get_default(&self) -> Result<&KeyProvider, OperationError> {
// In future we will make this configurable, and we'll load the default into
// the write txn during a reload.
self.get_uuid(UUID_KEY_PROVIDER_INTERNAL)
.ok_or(OperationError::KP0007KeyProviderDefaultNotAvailable)
}
pub(crate) fn get_or_create_in_default(
&mut self,
key_object_uuid: Uuid,
) -> Result<KeyObject, OperationError> {
self.get_or_create(UUID_KEY_PROVIDER_INTERNAL, key_object_uuid)
}
pub(crate) fn get_or_create(
&mut self,
key_provider_uuid: Uuid,
key_object_uuid: Uuid,
) -> Result<KeyObject, OperationError> {
if let Some(key_object) = self.inner.deref().objects.get(&key_object_uuid) {
Ok(key_object.as_ref().duplicate())
} else {
let provider = self
.inner
.deref()
.providers
.get(&key_provider_uuid)
.map(|k| k.as_ref())
.ok_or(OperationError::KP0025KeyProviderNotAvailable)?;
provider.create_new_key_object(key_object_uuid)
}
}
}
impl<'a> KeyProvidersWriteTransaction<'a> {
pub(crate) fn update_providers(
&mut self,
providers: Vec<Arc<KeyProvider>>,
) -> Result<(), OperationError> {
// Clear the current set.
self.inner.providers.clear();
// For each provider insert.
for provider in providers.into_iter() {
let uuid = provider.uuid();
if self.inner.providers.insert(uuid, provider).is_some() {
error!(key_provider_uuid = ?uuid, "duplicate key provider detected");
return Err(OperationError::KP0005KeyProviderDuplicate);
}
}
Ok(())
}
pub(crate) fn load_key_object(
&mut self,
entry: &EntrySealedCommitted,
) -> Result<(), OperationError> {
// Object UUID
let object_uuid = entry.get_uuid();
if !entry.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into()) {
error!(?object_uuid, "Invalid entry, keyobject class not found.");
return Err(OperationError::KP0011KeyObjectMissingClass);
}
// Get provider UUID.
let provider_uuid = entry
.get_ava_single_refer(Attribute::KeyProvider)
.ok_or_else(|| {
error!(
?object_uuid,
"Invalid key object, key provider referenced is not found."
);
OperationError::KP0012KeyObjectMissingProvider
})?;
let provider = self.inner.providers.get(&provider_uuid).ok_or_else(|| {
error!(
?object_uuid,
?provider_uuid,
"Invalid reference state, key provider has not be loaded."
);
OperationError::KP0012KeyProviderNotLoaded
})?;
// Ask the provider to load this object.
let key_object = provider.load_key_object(entry)?;
// Can't be duplicate as uuid is enforced unique in other layers.
self.inner.objects.insert(object_uuid, key_object);
Ok(())
}
pub(crate) fn commit(self) -> Result<(), OperationError> {
self.inner.commit();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{KeyProvider, KeyProvidersTransaction};
use crate::prelude::*;
use crate::value::KeyStatus;
use compact_jwt::{JwsEs256Signer, JwsSigner};
#[qs_test(domain_level=DOMAIN_LEVEL_5)]
async fn test_key_provider_internal_migration(server: &QueryServer) {
let mut write_txn = server.write(duration_from_epoch_now()).await;
// Read the initial state of the domain object, including it's
// private key.
let domain_object_initial = write_txn
.internal_search_uuid(UUID_DOMAIN_INFO)
.expect("unable to access domain object");
let initial_private_es256_key = domain_object_initial
.get_ava_single_private_binary(Attribute::Es256PrivateKeyDer)
.map(|s| s.to_vec())
.expect("No private key found");
let initial_jwt_signer =
JwsEs256Signer::from_es256_der(&initial_private_es256_key).unwrap();
let former_kid = initial_jwt_signer.get_legacy_kid().to_string();
// Set the version to 6.
write_txn
.internal_apply_domain_migration(DOMAIN_LEVEL_6)
.expect("Unable to set domain level to version 6");
// The internel key provider is created from dl 5 to 6
let key_provider_object = write_txn
.internal_search_uuid(UUID_KEY_PROVIDER_INTERNAL)
.expect("Unable to find key provider entry.");
assert!(key_provider_object.attribute_equality(
Attribute::Name,
&PartialValue::new_iname("key_provider_internal")
));
// Check that is loaded in the qs.
let key_provider = write_txn
.get_key_providers()
.get_uuid(UUID_KEY_PROVIDER_INTERNAL)
.expect("Unable to access key provider object.");
// Because there is only one variant today ...
#[allow(irrefutable_let_patterns)]
let KeyProvider::Internal(key_provider_internal) = key_provider
else {
unreachable!()
};
// Run the providers internal test
assert!(key_provider_internal.test().is_ok());
// Now at this point, the domain object should now be a key object, and have it's
// keys migrated.
let key_object = write_txn
.get_key_providers()
.get_key_object(UUID_DOMAIN_INFO)
.expect("Unable to retrieve key object by uuid");
// Assert the former key is now in the domain key object, and now is "retained".
let status = key_object
.kid_status(&former_kid)
.expect("Failed to access kid status");
assert_eq!(status, Some(KeyStatus::Retained));
// Now from DL6 -> 7 the keys are actually removed.
write_txn
.internal_apply_domain_migration(DOMAIN_LEVEL_7)
.expect("Unable to set domain level to version 7");
let domain_object_migrated = write_txn
.internal_search_uuid(UUID_DOMAIN_INFO)
.expect("unable to access domain object");
assert!(!domain_object_migrated.attribute_pres(Attribute::Es256PrivateKeyDer));
assert!(!domain_object_migrated.attribute_pres(Attribute::FernetPrivateKeyStr));
assert!(!domain_object_migrated.attribute_pres(Attribute::PrivateCookieKey));
write_txn.commit().expect("Failed to commit");
}
}

View file

@ -223,6 +223,21 @@ impl QueryServer {
}
impl<'a> QueryServerWriteTransaction<'a> {
/// Apply a domain migration `to_level`. Panics if `to_level` is not greater than the active
/// level.
#[cfg(test)]
pub(crate) fn internal_apply_domain_migration(
&mut self,
to_level: u32,
) -> Result<(), OperationError> {
assert!(to_level > self.get_domain_version());
self.internal_modify_uuid(
UUID_DOMAIN_INFO,
&ModifyList::new_purge_and_set(Attribute::Version, Value::new_uint32(to_level)),
)
.and_then(|()| self.reload())
}
#[instrument(level = "debug", skip_all)]
pub fn internal_migrate_or_create_str(&mut self, e_str: &str) -> Result<(), OperationError> {
let res = Entry::from_proto_entry_str(e_str, self)
@ -689,12 +704,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
admin_error!(?res, "migrate 16 to 17 -> result");
}
debug_assert!(res.is_ok());
res?;
self.changed_schema = true;
self.changed_acp = true;
Ok(())
res
}
#[instrument(level = "info", skip_all)]
@ -872,8 +882,21 @@ impl<'a> QueryServerWriteTransaction<'a> {
let idm_schema_classes = [
SCHEMA_ATTR_LIMIT_SEARCH_MAX_RESULTS_DL6.clone().into(),
SCHEMA_ATTR_LIMIT_SEARCH_MAX_FILTER_TEST_DL6.clone().into(),
SCHEMA_ATTR_KEY_INTERNAL_DATA_DL6.clone().into(),
SCHEMA_ATTR_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_ROTATE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_REVOKE_DL6.clone().into(),
SCHEMA_ATTR_KEY_ACTION_IMPORT_JWS_ES256_DL6.clone().into(),
SCHEMA_CLASS_ACCOUNT_POLICY_DL6.clone().into(),
SCHEMA_CLASS_DOMAIN_INFO_DL6.clone().into(),
SCHEMA_CLASS_SERVICE_ACCOUNT_DL6.clone().into(),
SCHEMA_CLASS_SYNC_ACCOUNT_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_DL6.clone().into(),
SCHEMA_CLASS_KEY_PROVIDER_INTERNAL_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWT_ES256_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_JWE_A128GCM_DL6.clone().into(),
SCHEMA_CLASS_KEY_OBJECT_INTERNAL_DL6.clone().into(),
];
idm_schema_classes
@ -886,17 +909,18 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.reload()?;
let idm_access_controls = [
let idm_data = [
// Update access controls.
IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone().into(),
IDM_ACP_PEOPLE_CREATE_DL6.clone().into(),
IDM_ACP_GROUP_MANAGE_DL6.clone().into(),
// Update anonymous with the correct entry manager,
BUILTIN_ACCOUNT_ANONYMOUS_DL6.clone().into(),
// Add the internal key provider.
E_KEY_PROVIDER_INTERNAL_DL6.clone().into(),
];
self.reload()?;
idm_access_controls
idm_data
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
@ -904,7 +928,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
err
})?;
// all the built-in objects get a builtin class
// all existing built-in objects get a builtin class
let filter = f_lt(
Attribute::Uuid,
PartialValue::Uuid(DYNAMIC_RANGE_MINIMUM_UUID),
@ -913,6 +937,66 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.internal_modify(&filter!(filter), &modlist)?;
// Reload such that the new default key provider is loaded.
self.reload()?;
// Update the domain entry to contain it's key object, which can now be generated.
let idm_data = [
IDM_ACP_DOMAIN_ADMIN_DL6.clone().into(),
E_DOMAIN_INFO_DL6.clone().into(),
];
idm_data
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_5_to_6 -> Error");
err
})?;
// At this point we reload to show the new key objects on the domain.
self.reload()?;
// Migrate the domain key to a retained key on the key object.
let domain_es256_private_key = self.get_domain_es256_private_key().map_err(|err| {
error!(?err, "migrate_domain_5_to_6 -> Error");
err
})?;
// Migrate all service/scim account keys to the domain key for verification.
let filter = filter!(f_or!([
f_eq(Attribute::Class, EntryClass::ServiceAccount.into()),
f_eq(Attribute::Class, EntryClass::SyncAccount.into())
]));
let entry_keys_to_migrate = self.internal_search(filter)?;
let mut modlist = Vec::with_capacity(1 + entry_keys_to_migrate.len());
modlist.push(Modify::Present(
Attribute::KeyActionImportJwsEs256.into(),
Value::PrivateBinary(domain_es256_private_key),
));
for entry in entry_keys_to_migrate {
// In these entries, the keys are in JwsEs256PrivateKey.
if let Some(jws_signer) =
entry.get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
{
let es256_private_key = jws_signer.private_key_to_der().map_err(|err| {
error!(?err, uuid = ?entry.get_display_id(), "unable to convert signer to der");
OperationError::InvalidValueState
})?;
modlist.push(Modify::Present(
Attribute::KeyActionImportJwsEs256.into(),
Value::PrivateBinary(es256_private_key),
));
}
}
let modlist = ModifyList::new_list(modlist);
self.internal_modify_uuid(UUID_DOMAIN_INFO, &modlist)?;
Ok(())
}
@ -1008,6 +1092,48 @@ impl<'a> QueryServerWriteTransaction<'a> {
// =========== Apply changes ==============
// Do this before schema change since domain info has cookie key
// as may at this point.
//
// Domain info should have the attribute private cookie key removed.
let modlist = ModifyList::new_list(vec![
Modify::Purged(Attribute::PrivateCookieKey.into()),
Modify::Purged(Attribute::Es256PrivateKeyDer.into()),
Modify::Purged(Attribute::FernetPrivateKeyStr.into()),
]);
self.internal_modify_uuid(UUID_DOMAIN_INFO, &modlist)?;
let filter = filter!(f_or!([
f_eq(Attribute::Class, EntryClass::ServiceAccount.into()),
f_eq(Attribute::Class, EntryClass::SyncAccount.into())
]));
let modlist =
ModifyList::new_list(vec![Modify::Purged(Attribute::JwsEs256PrivateKey.into())]);
self.internal_modify(&filter, &modlist)?;
// Now update schema
let idm_schema_classes = [
SCHEMA_CLASS_DOMAIN_INFO_DL7.clone().into(),
SCHEMA_CLASS_SERVICE_ACCOUNT_DL7.clone().into(),
SCHEMA_CLASS_SYNC_ACCOUNT_DL7.clone().into(),
];
idm_schema_classes
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry))
.map_err(|err| {
error!(?err, "migrate_domain_6_to_7 -> Error");
err
})?;
self.reload()?;
// Post schema changes.
Ok(())
}
@ -1275,15 +1401,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
admin_error!(?res, "initialise_idm p3 -> result");
}
debug_assert!(res.is_ok());
res?;
// Some attributes we don't want to stomp if they already exist. So we conditionally
// modify them.
self.changed_schema = true;
self.changed_acp = true;
Ok(())
res
}
}
@ -1481,4 +1599,48 @@ mod tests {
write_txn.commit().expect("Unable to commit");
}
#[qs_test(domain_level=DOMAIN_LEVEL_6)]
async fn test_migrations_dl6_dl7(server: &QueryServer) {
// Assert our instance was setup to version 6
let mut write_txn = server.write(duration_from_epoch_now()).await;
let db_domain_version = write_txn
.internal_search_uuid(UUID_DOMAIN_INFO)
.expect("unable to access domain entry")
.get_ava_single_uint32(Attribute::Version)
.expect("Attribute Version not present");
assert_eq!(db_domain_version, DOMAIN_LEVEL_6);
// per migration verification.
let domain_entry = write_txn
.internal_search_uuid(UUID_DOMAIN_INFO)
.expect("Unable to access domain entry");
assert!(domain_entry.attribute_pres(Attribute::PrivateCookieKey));
// Set the version to 7.
write_txn
.internal_modify_uuid(
UUID_DOMAIN_INFO,
&ModifyList::new_purge_and_set(
Attribute::Version,
Value::new_uint32(DOMAIN_LEVEL_7),
),
)
.expect("Unable to set domain level to version 7");
// Re-load - this applies the migrations.
write_txn.reload().expect("Unable to reload transaction");
// post migration verification.
let domain_entry = write_txn
.internal_search_uuid(UUID_DOMAIN_INFO)
.expect("Unable to access domain entry");
assert!(!domain_entry.attribute_pres(Attribute::PrivateCookieKey));
write_txn.commit().expect("Unable to commit");
}
}

View file

@ -37,11 +37,17 @@ use self::access::{
AccessControlsWriteTransaction,
};
use self::keys::{
KeyObject, KeyProvider, KeyProviders, KeyProvidersReadTransaction, KeyProvidersTransaction,
KeyProvidersWriteTransaction,
};
pub(crate) mod access;
pub mod batch_modify;
pub mod create;
pub mod delete;
pub mod identity;
pub(crate) mod keys;
pub(crate) mod migrations;
pub mod modify;
pub(crate) mod recycle;
@ -89,6 +95,7 @@ pub struct QueryServer {
Arc<ARCache<(IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
dyngroup_cache: Arc<CowCell<DynGroupCache>>,
cid_max: Arc<CowCell<Cid>>,
key_providers: Arc<KeyProviders>,
}
pub struct QueryServerReadTransaction<'a> {
@ -99,6 +106,7 @@ pub struct QueryServerReadTransaction<'a> {
system_config: CowCellReadTxn<SystemConfig>,
schema: SchemaReadTransaction,
accesscontrols: AccessControlsReadTransaction<'a>,
key_providers: KeyProvidersReadTransaction,
_db_ticket: SemaphorePermit<'a>,
resolve_filter_cache:
ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>,
@ -111,6 +119,19 @@ unsafe impl<'a> Sync for QueryServerReadTransaction<'a> {}
unsafe impl<'a> Send for QueryServerReadTransaction<'a> {}
bitflags::bitflags! {
#[derive(Copy, Clone, Debug)]
pub struct ChangeFlag: u32 {
const SCHEMA = 0b0000_0001;
const ACP = 0b0000_0010;
const OAUTH2 = 0b0000_0100;
const DOMAIN = 0b0000_1000;
const SYSTEM_CONFIG = 0b0001_0000;
const SYNC_AGREEMENT = 0b0010_0000;
const KEY_MATERIAL = 0b0100_0000;
}
}
pub struct QueryServerWriteTransaction<'a> {
committed: bool,
phase: CowCellWriteTxn<'a, ServerPhase>,
@ -122,15 +143,12 @@ pub struct QueryServerWriteTransaction<'a> {
pub(crate) be_txn: BackendWriteTransaction<'a>,
pub(crate) schema: SchemaWriteTransaction<'a>,
accesscontrols: AccessControlsWriteTransaction<'a>,
key_providers: KeyProvidersWriteTransaction<'a>,
// We store a set of flags that indicate we need a reload of
// schema or acp, which is tested by checking the classes of the
// changing content.
pub(super) changed_schema: bool,
pub(super) changed_acp: bool,
pub(super) changed_oauth2: bool,
pub(super) changed_domain: bool,
pub(super) changed_system_config: bool,
pub(super) changed_sync_agreement: bool,
pub(super) changed_flags: ChangeFlag,
// Store the list of changed uuids for other invalidation needs?
pub(super) changed_uuid: HashSet<Uuid>,
_db_ticket: SemaphorePermit<'a>,
@ -165,6 +183,9 @@ pub trait QueryServerTransaction<'a> {
type AccessControlsTransactionType: AccessControlsTransaction<'a>;
fn get_accesscontrols(&self) -> &Self::AccessControlsTransactionType;
type KeyProvidersTransactionType: KeyProvidersTransaction;
fn get_key_providers(&self) -> &Self::KeyProvidersTransactionType;
fn pw_badlist(&self) -> &HashSet<String>;
fn denied_names(&self) -> &HashSet<String>;
@ -602,6 +623,9 @@ pub trait QueryServerTransaction<'a> {
SyntaxType::TotpSecret => Err(OperationError::InvalidAttribute("TotpSecret Values can not be supplied through modification".to_string())),
SyntaxType::AuditLogString => Err(OperationError::InvalidAttribute("Audit logs are generated and not able to be set.".to_string())),
SyntaxType::EcKeyPrivate => Err(OperationError::InvalidAttribute("Ec keys are generated and not able to be set.".to_string())),
SyntaxType::KeyInternal => Err(OperationError::InvalidAttribute("Internal keys are generated and not able to be set.".to_string())),
SyntaxType::HexString => Value::new_hex_string_s(value)
.ok_or_else(|| OperationError::InvalidAttribute("Invalid hex string syntax".to_string())),
}
}
None => {
@ -725,6 +749,13 @@ pub trait QueryServerTransaction<'a> {
SyntaxType::WebauthnAttestationCaList => Err(OperationError::InvalidAttribute(
"Invalid - unable to query attestation CA list".to_string(),
)),
SyntaxType::HexString | SyntaxType::KeyInternal => {
PartialValue::new_hex_string_s(value).ok_or_else(|| {
OperationError::InvalidAttribute(
"Invalid key identifer syntax, expected hex string".to_string(),
)
})
}
}
}
None => {
@ -834,17 +865,17 @@ pub trait QueryServerTransaction<'a> {
})
}
fn get_domain_fernet_private_key(&mut self) -> Result<String, OperationError> {
self.internal_search_uuid(UUID_DOMAIN_INFO)
.and_then(|e| {
e.get_ava_single_secret(Attribute::FernetPrivateKeyStr)
.map(str::to_string)
.ok_or(OperationError::InvalidEntryState)
})
.map_err(|e| {
admin_error!(?e, "Error getting domain fernet key");
e
})
fn get_domain_key_object_handle(&self) -> Result<Arc<KeyObject>, OperationError> {
#[cfg(test)]
if self.get_domain_version() < DOMAIN_LEVEL_6 {
// We must be in tests, and this is a DL5 to 6 test. For this we'll just make
// an ephemeral provider.
return Ok(crate::server::keys::KeyObjectInternal::new_test());
};
self.get_key_providers()
.get_key_object_handle(UUID_DOMAIN_INFO)
.ok_or(OperationError::KP0031KeyObjectNotFound)
}
fn get_domain_es256_private_key(&mut self) -> Result<Vec<u8>, OperationError> {
@ -859,6 +890,7 @@ pub trait QueryServerTransaction<'a> {
e
})
}
fn get_domain_ldap_allow_unix_pw_bind(&mut self) -> Result<bool, OperationError> {
self.internal_search_uuid(UUID_DOMAIN_INFO).map(|entry| {
entry
@ -866,26 +898,6 @@ pub trait QueryServerTransaction<'a> {
.unwrap_or(true)
})
}
fn get_domain_cookie_key(&mut self) -> Result<[u8; 64], OperationError> {
self.internal_search_uuid(UUID_DOMAIN_INFO)
.and_then(|e| {
e.get_ava_single_private_binary(Attribute::PrivateCookieKey)
.and_then(|s| {
let mut x = [0; 64];
if s.len() == x.len() {
x.copy_from_slice(s);
Some(x)
} else {
None
}
})
.ok_or(OperationError::InvalidEntryState)
})
.map_err(|e| {
admin_error!(?e, "Error getting domain cookie key");
e
})
}
/// Get the password badlist from the system config. You should not call this directly
/// as this value is cached in the system_config() value.
@ -977,6 +989,7 @@ impl<'a> QueryServerTransaction<'a> for QueryServerReadTransaction<'a> {
type AccessControlsTransactionType = AccessControlsReadTransaction<'a>;
type BackendTransactionType = BackendReadTransaction<'a>;
type SchemaTransactionType = SchemaReadTransaction;
type KeyProvidersTransactionType = KeyProvidersReadTransaction;
fn get_be_txn(&mut self) -> &mut BackendReadTransaction<'a> {
&mut self.be_txn
@ -996,6 +1009,10 @@ impl<'a> QueryServerTransaction<'a> for QueryServerReadTransaction<'a> {
&self.accesscontrols
}
fn get_key_providers(&self) -> &KeyProvidersReadTransaction {
&self.key_providers
}
fn get_resolve_filter_cache(
&mut self,
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
@ -1118,6 +1135,7 @@ impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
type AccessControlsTransactionType = AccessControlsWriteTransaction<'a>;
type BackendTransactionType = BackendWriteTransaction<'a>;
type SchemaTransactionType = SchemaWriteTransaction<'a>;
type KeyProvidersTransactionType = KeyProvidersWriteTransaction<'a>;
fn get_be_txn(&mut self) -> &mut BackendWriteTransaction<'a> {
&mut self.be_txn
@ -1137,6 +1155,10 @@ impl<'a> QueryServerTransaction<'a> for QueryServerWriteTransaction<'a> {
&self.accesscontrols
}
fn get_key_providers(&self) -> &KeyProvidersWriteTransaction<'a> {
&self.key_providers
}
fn get_resolve_filter_cache(
&mut self,
) -> &mut ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>, ()>
@ -1234,6 +1256,8 @@ impl QueryServer {
.expect("Failed to build resolve_filter_cache"),
);
let key_providers = Arc::new(KeyProviders::default());
Ok(QueryServer {
phase,
d_info,
@ -1246,6 +1270,7 @@ impl QueryServer {
resolve_filter_cache,
dyngroup_cache,
cid_max,
key_providers: key_providers,
})
}
@ -1288,6 +1313,7 @@ impl QueryServer {
d_info: self.d_info.read(),
system_config: self.system_config.read(),
accesscontrols: self.accesscontrols.read(),
key_providers: self.key_providers.read(),
_db_ticket: db_ticket,
resolve_filter_cache: self.resolve_filter_cache.read(),
trim_cid,
@ -1359,17 +1385,13 @@ impl QueryServer {
be_txn,
schema: schema_write,
accesscontrols: self.accesscontrols.write(),
changed_schema: false,
changed_acp: false,
changed_oauth2: false,
changed_domain: false,
changed_system_config: false,
changed_sync_agreement: false,
changed_flags: ChangeFlag::empty(),
changed_uuid: HashSet::new(),
_db_ticket: db_ticket,
_write_ticket: write_ticket,
resolve_filter_cache: self.resolve_filter_cache.read(),
dyngroup_cache: self.dyngroup_cache.write(),
key_providers: self.key_providers.write(),
}
}
@ -1414,6 +1436,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
&self.cid
}
pub(crate) fn get_key_providers_mut(&mut self) -> &mut KeyProvidersWriteTransaction<'a> {
&mut self.key_providers
}
pub(crate) fn get_dyngroup_cache(&mut self) -> &mut DynGroupCache {
&mut self.dyngroup_cache
}
@ -1444,7 +1470,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
level, mut_d_info.d_vers
);
mut_d_info.d_vers = level;
self.changed_domain = true;
self.changed_flags.insert(ChangeFlag::DOMAIN);
Ok(())
}
@ -1685,6 +1711,41 @@ impl<'a> QueryServerWriteTransaction<'a> {
})
}
#[instrument(level = "debug", skip_all)]
pub(crate) fn reload_key_material(&mut self) -> Result<(), OperationError> {
let filt = filter!(f_eq(Attribute::Class, EntryClass::KeyProvider.into()));
let res = self.internal_search(filt).map_err(|e| {
admin_error!(
err = ?e,
"reload key providers internal search failed",
);
e
})?;
// FUTURE: During this reload we may need to access the PIN or other data
// to access the provider.
let providers = res
.iter()
.map(|e| KeyProvider::try_from(e).and_then(|kp| kp.test().map(|()| kp)))
.collect::<Result<Vec<_>, _>>()?;
self.key_providers.update_providers(providers)?;
let filt = filter!(f_eq(Attribute::Class, EntryClass::KeyObject.into()));
let res = self.internal_search(filt).map_err(|e| {
admin_error!(
err = ?e,
"reload key objects internal search failed",
);
e
})?;
res.iter()
.try_for_each(|entry| self.key_providers.load_key_object(entry.as_ref()))
}
#[instrument(level = "debug", skip_all)]
pub(crate) fn reload_system_config(&mut self) -> Result<(), OperationError> {
let denied_names = self.get_sc_denied_names()?;
@ -1711,7 +1772,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
})?;
// We have to set the domain version here so that features which check for it
// will now see it's been increased.
// will now see it's been increased. This also prevents recursion during reloads
// inside of a domain migration.
let mut_d_info = self.d_info.get_mut();
let previous_version = mut_d_info.d_vers;
mut_d_info.d_vers = domain_info_version;
@ -1738,6 +1800,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.migrate_domain_5_to_6()?;
}
if previous_version <= DOMAIN_LEVEL_6 && domain_info_version >= DOMAIN_LEVEL_7 {
self.migrate_domain_6_to_7()?;
}
// This is here to catch when we increase domain levels but didn't create the migration
// hooks. If this fails it probably means you need to add another migration hook
// in the above.
debug_assert!(domain_info_version <= DOMAIN_LEVEL_7);
Ok(())
}
@ -1820,19 +1891,21 @@ impl<'a> QueryServerWriteTransaction<'a> {
}
fn force_schema_reload(&mut self) {
self.changed_schema = true;
self.changed_flags.insert(ChangeFlag::SCHEMA);
}
pub(crate) fn upgrade_reindex(&mut self, v: i64) -> Result<(), OperationError> {
self.be_txn.upgrade_reindex(v)
}
#[inline]
pub(crate) fn get_changed_oauth2(&self) -> bool {
self.changed_oauth2
self.changed_flags.contains(ChangeFlag::OAUTH2)
}
pub(crate) fn get_changed_domain(&self) -> bool {
self.changed_domain
#[inline]
pub(crate) fn clear_changed_oauth2(&mut self) {
self.changed_flags.remove(ChangeFlag::OAUTH2)
}
fn set_phase(&mut self, phase: ServerPhase) {
@ -1846,7 +1919,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
pub(crate) fn reload(&mut self) -> Result<(), OperationError> {
// First, check if the domain version has changed. This can trigger
// changes to schema, access controls and more.
if self.changed_domain {
if self.changed_flags.contains(ChangeFlag::DOMAIN) {
self.reload_domain_info_version()?;
}
@ -1854,7 +1927,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
// in an operation so we can check if we need to do the reload or not
//
// Reload the schema from qs.
if self.changed_schema {
if self.changed_flags.contains(ChangeFlag::SCHEMA) {
self.reload_schema()?;
// If the server is in a late phase of start up or is
@ -1866,6 +1939,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
}
}
// We need to reload cryptographic providers before anything else so that
// sync agreements and the domain can access their key material.
if self
.changed_flags
.intersects(ChangeFlag::SCHEMA | ChangeFlag::KEY_MATERIAL)
{
self.reload_key_material()?;
}
// Determine if we need to update access control profiles
// based on any modifications that have occurred.
// IF SCHEMA CHANGED WE MUST ALSO RELOAD!!! IE if schema had an attr removed
@ -1873,7 +1955,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
//
// Also note that changing sync agreements triggers an acp reload since
// access controls need to be aware of these agreements.
if self.changed_schema || self.changed_acp || self.changed_sync_agreement {
if self
.changed_flags
.intersects(ChangeFlag::SCHEMA | ChangeFlag::ACP | ChangeFlag::SYNC_AGREEMENT)
{
self.reload_accesscontrols()?;
} else {
// On a reload the cache is dropped, otherwise we tell accesscontrols
@ -1882,20 +1967,23 @@ impl<'a> QueryServerWriteTransaction<'a> {
// .invalidate_related_cache(self.changed_uuid.into_inner().as_slice())
}
if self.changed_system_config {
if self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG) {
self.reload_system_config()?;
}
if self.changed_domain {
if self.changed_flags.contains(ChangeFlag::DOMAIN) {
self.reload_domain_info()?;
}
// Clear flags
self.changed_domain = false;
self.changed_schema = false;
self.changed_system_config = false;
self.changed_acp = false;
self.changed_sync_agreement = false;
self.changed_flags.remove(
ChangeFlag::DOMAIN
| ChangeFlag::SCHEMA
| ChangeFlag::SYSTEM_CONFIG
| ChangeFlag::ACP
| ChangeFlag::SYNC_AGREEMENT
| ChangeFlag::KEY_MATERIAL,
);
Ok(())
}
@ -1921,22 +2009,24 @@ impl<'a> QueryServerWriteTransaction<'a> {
accesscontrols,
cid,
dyngroup_cache,
key_providers,
// Hold these for a bit more ...
_db_ticket,
_write_ticket,
// Ignore values that don't need a commit.
curtime: _,
trim_cid: _,
changed_schema: _,
changed_acp: _,
changed_oauth2: _,
changed_domain: _,
changed_system_config: _,
changed_sync_agreement: _,
changed_flags,
changed_uuid: _,
_db_ticket: _,
_write_ticket: _,
resolve_filter_cache: _,
} = self;
debug_assert!(!committed);
// Should have been cleared by any reloads.
trace!(
changed = ?changed_flags.iter_names().collect::<Vec<_>>(),
);
// Write the cid to the db. If this fails, we can't assume replication
// will be stable, so return if it fails.
be_txn.set_db_ts_max(cid.ts)?;
@ -1952,6 +2042,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
.map(|_| system_config.commit())
.map(|_| phase.commit())
.map(|_| dyngroup_cache.commit())
.and_then(|_| key_providers.commit())
.and_then(|_| accesscontrols.commit())
.and_then(|_| be_txn.commit())
}

View file

@ -1,5 +1,6 @@
use std::sync::Arc;
use super::ChangeFlag;
use crate::plugins::Plugins;
use crate::prelude::*;
@ -21,7 +22,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
}
}
/// Unsafety: This is unsafe because you need to be careful about how you handle and check
/// SAFETY: This is unsafe because you need to be careful about how you handle and check
/// the Ok(None) case which occurs during internal operations, and that you DO NOT re-order
/// and call multiple pre-applies at the same time, else you can cause DB corruption.
#[instrument(level = "debug", skip_all)]
@ -189,25 +190,32 @@ impl<'a> QueryServerWriteTransaction<'a> {
// We have finished all plugs and now have a successful operation - flag if
// schema or acp requires reload. Remember, this is a modify, so we need to check
// pre and post cands.
if !self.changed_schema {
self.changed_schema = norm_cand
if !self.changed_flags.contains(ChangeFlag::SCHEMA)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
});
})
{
self.changed_flags.insert(ChangeFlag::SCHEMA)
}
if !self.changed_acp {
self.changed_acp = norm_cand
if !self.changed_flags.contains(ChangeFlag::ACP)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
})
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_oauth2 {
self.changed_oauth2 = norm_cand
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& norm_cand
.iter()
.zip(pre_candidates.iter().map(|e| e.as_ref()))
.any(|(post, pre)| {
@ -224,25 +232,48 @@ impl<'a> QueryServerWriteTransaction<'a> {
&EntryClass::OAuth2ResourceServer.into(),
)) && post
.entry_changed_excluding_attribute(Attribute::OAuth2Session, &self.cid)
});
})
{
self.changed_flags.insert(ChangeFlag::OAUTH2)
}
if !self.changed_domain {
self.changed_domain = norm_cand
if !self.changed_flags.contains(ChangeFlag::DOMAIN)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
{
self.changed_flags.insert(ChangeFlag::DOMAIN)
}
if !self.changed_system_config {
self.changed_system_config = norm_cand
if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
{
self.changed_flags.insert(ChangeFlag::SYSTEM_CONFIG)
}
if !self.changed_sync_agreement {
self.changed_sync_agreement = norm_cand
if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()));
.any(|e| e.attribute_equality(Attribute::Class, &EntryClass::SyncAccount.into()))
{
self.changed_flags.insert(ChangeFlag::SYNC_AGREEMENT)
}
if !self.changed_flags.contains(ChangeFlag::KEY_MATERIAL)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::KeyProvider.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::KeyObject.into())
})
{
self.changed_flags.insert(ChangeFlag::KEY_MATERIAL)
}
self.changed_uuid.extend(
@ -253,12 +284,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
);
trace!(
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
system_config_reload = ?self.changed_system_config,
changed_sync_agreement = ?self.changed_sync_agreement
changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
);
// return
@ -367,50 +393,59 @@ impl<'a> QueryServerWriteTransaction<'a> {
e
})?;
if !self.changed_schema {
self.changed_schema = norm_cand
if !self.changed_flags.contains(ChangeFlag::SCHEMA)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::ClassType.into())
|| e.attribute_equality(Attribute::Class, &EntryClass::AttributeType.into())
});
})
{
self.changed_flags.insert(ChangeFlag::SCHEMA)
}
if !self.changed_acp {
self.changed_acp = norm_cand
if !self.changed_flags.contains(ChangeFlag::ACP)
&& norm_cand
.iter()
.chain(pre_candidates.iter().map(|e| e.as_ref()))
.any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::AccessControlProfile.into())
});
})
{
self.changed_flags.insert(ChangeFlag::ACP)
}
if !self.changed_oauth2 {
self.changed_oauth2 = norm_cand.iter().any(|e| {
if !self.changed_flags.contains(ChangeFlag::OAUTH2)
&& norm_cand.iter().any(|e| {
e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
});
})
{
self.changed_flags.insert(ChangeFlag::OAUTH2)
}
if !self.changed_domain {
self.changed_domain = norm_cand
if !self.changed_flags.contains(ChangeFlag::DOMAIN)
&& norm_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_DOMAIN_INFO))
{
self.changed_flags.insert(ChangeFlag::DOMAIN)
}
if !self.changed_system_config {
self.changed_system_config = norm_cand
if !self.changed_flags.contains(ChangeFlag::SYSTEM_CONFIG)
&& norm_cand
.iter()
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG));
.any(|e| e.attribute_equality(Attribute::Uuid, &PVUUID_SYSTEM_CONFIG))
{
self.changed_flags.insert(ChangeFlag::DOMAIN)
}
self.changed_uuid.extend(
norm_cand
.iter()
.map(|e| e.get_uuid())
.chain(pre_candidates.iter().map(|e| e.get_uuid())),
);
trace!(
schema_reload = ?self.changed_schema,
acp_reload = ?self.changed_acp,
oauth2_reload = ?self.changed_oauth2,
domain_reload = ?self.changed_domain,
system_config_reload = ?self.changed_system_config,
changed = ?self.changed_flags.iter_names().collect::<Vec<_>>(),
);
trace!("Modify operation success");

View file

@ -39,6 +39,9 @@ use crate::repl::cid::Cid;
use crate::server::identity::IdentityId;
use crate::valueset::image::ImageValueThings;
use crate::valueset::uuid_to_proto_string;
use crate::server::keys::KeyId;
use kanidm_proto::internal::{ApiTokenPurpose, Filter as ProtoFilter, UiHint};
use kanidm_proto::v1::UatPurposeStatus;
use std::hash::Hash;
@ -64,6 +67,12 @@ lazy_static! {
Regex::new("^[a-z][a-z0-9-_\\.]*$").expect("Invalid Iname regex found")
};
/// Only lowercase+numbers, with limited chars.
pub static ref HEXSTR_RE: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^[a-f0-9]+$").expect("Invalid hexstring regex found")
};
pub static ref EXTRACT_VAL_DN: Regex = {
#[allow(clippy::expect_used)]
Regex::new("^(([^=,]+)=)?(?P<val>[^=,]+)").expect("extract val from dn regex")
@ -264,6 +273,8 @@ pub enum SyntaxType {
CredentialType = 35,
WebauthnAttestationCaList = 36,
OauthClaimMap = 37,
KeyInternal = 38,
HexString = 39,
}
impl TryFrom<&str> for SyntaxType {
@ -310,6 +321,8 @@ impl TryFrom<&str> for SyntaxType {
"CREDENTIAL_TYPE" => Ok(SyntaxType::CredentialType),
"WEBAUTHN_ATTESTATION_CA_LIST" => Ok(SyntaxType::WebauthnAttestationCaList),
"OAUTH_CLAIM_MAP" => Ok(SyntaxType::OauthClaimMap),
"KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
"HEX_STRING" => Ok(SyntaxType::HexString),
_ => Err(()),
}
}
@ -356,6 +369,8 @@ impl fmt::Display for SyntaxType {
SyntaxType::CredentialType => "CREDENTIAL_TYPE",
SyntaxType::WebauthnAttestationCaList => "WEBAUTHN_ATTESTATION_CA_LIST",
SyntaxType::OauthClaimMap => "OAUTH_CLAIM_MAP",
SyntaxType::KeyInternal => "KEY_INTERNAL",
SyntaxType::HexString => "HEX_STRING",
})
}
}
@ -476,6 +491,8 @@ pub enum PartialValue {
OauthClaim(String, Uuid),
OauthClaimValue(String, Uuid, String),
HexString(String),
}
impl From<SyntaxType> for PartialValue {
@ -791,6 +808,15 @@ impl PartialValue {
Uuid::parse_str(us).map(PartialValue::AttestedPasskey).ok()
}
pub fn new_hex_string_s(hexstr: &str) -> Option<Self> {
let hexstr_lower = hexstr.to_lowercase();
if HEXSTR_RE.is_match(&hexstr_lower) {
Some(PartialValue::HexString(hexstr_lower))
} else {
None
}
}
pub fn new_image(input: &str) -> Self {
PartialValue::Image(input.to_string())
}
@ -857,6 +883,7 @@ impl PartialValue {
// We don't allow searching on claim/uuid pairs.
PartialValue::OauthClaim(_, _) => "_".to_string(),
PartialValue::OauthClaimValue(_, _, _) => "_".to_string(),
PartialValue::HexString(hexstr) => hexstr.to_string(),
}
}
@ -1045,6 +1072,46 @@ pub struct Oauth2Session {
pub rs_uuid: Uuid,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyUsage {
JwsEs256,
JweA128GCM,
}
impl fmt::Display for KeyUsage {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
KeyUsage::JwsEs256 => "jws_es256",
KeyUsage::JweA128GCM => "jwe_a128gcm",
}
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum KeyStatus {
Valid,
Retained,
Revoked,
}
impl fmt::Display for KeyStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
KeyStatus::Valid => "valid",
KeyStatus::Retained => "retained",
KeyStatus::Revoked => "revoked",
}
)
}
}
/// 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.
@ -1103,6 +1170,17 @@ pub enum Value {
OauthClaimValue(String, Uuid, BTreeSet<String>),
OauthClaimMap(String, OauthClaimMapJoin),
KeyInternal {
id: KeyId,
usage: KeyUsage,
valid_from: u64,
status: KeyStatus,
status_cid: Cid,
der: Vec<u8>,
},
HexString(String),
}
impl PartialEq for Value {
@ -1384,6 +1462,15 @@ impl Value {
}
}
pub fn new_hex_string_s(hexstr: &str) -> Option<Self> {
let hexstr_lower = hexstr.to_lowercase();
if HEXSTR_RE.is_match(&hexstr_lower) {
Some(Value::HexString(hexstr_lower))
} else {
None
}
}
/// Want a `Value::Image`? use this!
pub fn new_image(input: &str) -> Result<Self, OperationError> {
serde_json::from_str::<ImageValue>(input)
@ -1913,6 +2000,12 @@ impl Value {
OAUTHSCOPE_RE.is_match(name) && value.iter().all(|s| OAUTHSCOPE_RE.is_match(s))
}
Value::HexString(id) | Value::KeyInternal { id, .. } => {
Value::validate_str_escapes(id.as_str())
&& Value::validate_singleline(id.as_str())
&& Value::validate_hexstr(id.as_str())
}
Value::PhoneNumber(_, _) => true,
Value::Address(_) => true,
@ -1961,6 +2054,15 @@ impl Value {
}
}
pub(crate) fn validate_hexstr(s: &str) -> bool {
if !HEXSTR_RE.is_match(s) {
error!("hexstrings may only contain limited characters. - \"{}\" does not pass regex pattern \"{}\"", s, *HEXSTR_RE);
false
} else {
true
}
}
pub(crate) fn validate_singleline(s: &str) -> bool {
if !SINGLELINE_RE.is_match(s) {
true
@ -2239,4 +2341,10 @@ mod tests {
assert!(!Value::validate_str_escapes("naughty \x1b[31mred"));
}
#[test]
fn test_value_key_internal_status_order() {
assert!(KeyStatus::Valid < KeyStatus::Retained);
assert!(KeyStatus::Retained < KeyStatus::Revoked);
}
}

View file

@ -0,0 +1,175 @@
use std::collections::BTreeSet;
use crate::prelude::*;
use crate::repl::proto::ReplAttrV1;
use crate::schema::SchemaAttribute;
use crate::valueset::{DbValueSetV2, ValueSet};
#[derive(Debug, Clone)]
pub struct ValueSetHexString {
set: BTreeSet<String>,
}
impl ValueSetHexString {
pub fn new(s: String) -> Box<Self> {
let mut set = BTreeSet::new();
set.insert(s);
Box::new(ValueSetHexString { set })
}
pub fn push(&mut self, s: &str) -> bool {
self.set.insert(s.to_lowercase())
}
pub fn from_dbvs2(data: Vec<String>) -> Result<ValueSet, OperationError> {
let set = data.into_iter().collect();
Ok(Box::new(ValueSetHexString { set }))
}
pub fn from_repl_v1(data: &[String]) -> Result<ValueSet, OperationError> {
let set = data.iter().cloned().collect();
Ok(Box::new(ValueSetHexString { set }))
}
// We need to allow this, because rust doesn't allow us to impl FromIterator on foreign
// types, and str is foreign
#[allow(clippy::should_implement_trait)]
pub fn from_iter<'a, T>(iter: T) -> Option<Box<Self>>
where
T: IntoIterator<Item = &'a str>,
{
let set = iter.into_iter().map(str::to_string).collect();
Some(Box::new(ValueSetHexString { set }))
}
}
impl ValueSetT for ValueSetHexString {
fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
match value {
Value::HexString(s) => Ok(self.set.insert(s)),
_ => {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
}
fn clear(&mut self) {
self.set.clear();
}
fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
match pv {
PartialValue::HexString(s) => self.set.remove(s),
_ => {
debug_assert!(false);
true
}
}
}
fn contains(&self, pv: &PartialValue) -> bool {
match pv {
PartialValue::HexString(s) => self.set.contains(s.as_str()),
_ => false,
}
}
fn substring(&self, pv: &PartialValue) -> bool {
match pv {
PartialValue::HexString(s2) => self.set.iter().any(|s1| s1.contains(s2)),
_ => {
debug_assert!(false);
false
}
}
}
fn startswith(&self, pv: &PartialValue) -> bool {
match pv {
PartialValue::HexString(s2) => self.set.iter().any(|s1| s1.starts_with(s2)),
_ => {
debug_assert!(false);
false
}
}
}
fn endswith(&self, pv: &PartialValue) -> bool {
match pv {
PartialValue::HexString(s2) => self.set.iter().any(|s1| s1.ends_with(s2)),
_ => {
debug_assert!(false);
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().cloned().collect()
}
fn syntax(&self) -> SyntaxType {
SyntaxType::HexString
}
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
self.set.iter().all(|s| {
Value::validate_str_escapes(s.as_str())
&& Value::validate_singleline(s.as_str())
&& Value::validate_hexstr(s.as_str())
})
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(self.set.iter().cloned())
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
DbValueSetV2::HexString(self.set.iter().cloned().collect())
}
fn to_repl_v1(&self) -> ReplAttrV1 {
ReplAttrV1::HexString {
set: self.set.iter().cloned().collect(),
}
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
Box::new(self.set.iter().cloned().map(PartialValue::HexString))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
Box::new(self.set.iter().cloned().map(Value::HexString))
}
fn equal(&self, other: &ValueSet) -> bool {
if let Some(other) = other.as_hexstring_set() {
&self.set == other
} else {
debug_assert!(false);
false
}
}
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
if let Some(b) = other.as_hexstring_set() {
mergesets!(self.set, b)
} else {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
fn as_hexstring_set(&self) -> Option<&BTreeSet<String>> {
Some(&self.set)
}
}

View file

@ -0,0 +1,622 @@
use crate::prelude::*;
use crate::repl::proto::ReplAttrV1;
use crate::server::keys::KeyId;
use crate::value::{KeyStatus, KeyUsage};
use crate::be::dbvalue::{DbValueKeyInternal, DbValueKeyStatus, DbValueKeyUsage};
use crate::valueset::{DbValueSetV2, ValueSet};
use std::collections::BTreeMap;
use std::fmt;
#[derive(Clone, PartialEq, Eq)]
pub struct KeyInternalData {
pub usage: KeyUsage,
pub valid_from: u64,
pub status: KeyStatus,
pub status_cid: Cid,
pub der: Vec<u8>,
}
impl fmt::Debug for KeyInternalData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeyInternalData")
.field("usage", &self.usage)
.field("valid_from", &self.valid_from)
.field("status", &self.status)
.field("status_cid", &self.status_cid)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ValueSetKeyInternal {
map: BTreeMap<KeyId, KeyInternalData>,
}
impl ValueSetKeyInternal {
pub fn new(
id: KeyId,
usage: KeyUsage,
valid_from: u64,
status: KeyStatus,
status_cid: Cid,
der: Vec<u8>,
) -> Box<Self> {
let map = BTreeMap::from([(
id,
KeyInternalData {
usage,
valid_from,
status,
status_cid,
der,
},
)]);
Box::new(ValueSetKeyInternal { map })
}
pub fn from_key_iter(
keys: impl Iterator<Item = (KeyId, KeyInternalData)>,
) -> Result<ValueSet, OperationError> {
let map = keys.collect();
Ok(Box::new(ValueSetKeyInternal { map }))
}
fn from_dbv_iter(
keys: impl Iterator<Item = DbValueKeyInternal>,
) -> Result<ValueSet, OperationError> {
let map = keys
.map(|dbv_key| {
match dbv_key {
DbValueKeyInternal::V1 {
id,
usage,
valid_from,
status,
status_cid,
der,
} => {
// Type cast, for now, these are both Vec<u8>
let id: KeyId = id;
let usage = match usage {
DbValueKeyUsage::JwsEs256 => KeyUsage::JwsEs256,
DbValueKeyUsage::JweA128GCM => KeyUsage::JweA128GCM,
};
let status_cid = status_cid.into();
let status = match status {
DbValueKeyStatus::Valid => KeyStatus::Valid,
DbValueKeyStatus::Retained => KeyStatus::Retained,
DbValueKeyStatus::Revoked => KeyStatus::Revoked,
};
Ok((
id,
KeyInternalData {
usage,
valid_from,
status,
status_cid,
der,
},
))
}
}
})
.collect::<Result<BTreeMap<_, _>, _>>()?;
Ok(Box::new(ValueSetKeyInternal { map }))
}
pub fn from_dbvs2(keys: Vec<DbValueKeyInternal>) -> Result<ValueSet, OperationError> {
Self::from_dbv_iter(keys.into_iter())
}
pub fn from_repl_v1(keys: &[DbValueKeyInternal]) -> Result<ValueSet, OperationError> {
Self::from_dbv_iter(keys.iter().cloned())
}
fn to_vec_dbvs(&self) -> Vec<DbValueKeyInternal> {
self.map
.iter()
.map(
|(
id,
KeyInternalData {
usage,
status,
status_cid,
valid_from,
der,
},
)| {
let id: String = id.clone();
let usage = match usage {
KeyUsage::JwsEs256 => DbValueKeyUsage::JwsEs256,
KeyUsage::JweA128GCM => DbValueKeyUsage::JweA128GCM,
};
let status_cid = status_cid.into();
let status = match status {
KeyStatus::Valid => DbValueKeyStatus::Valid,
KeyStatus::Retained => DbValueKeyStatus::Retained,
KeyStatus::Revoked => DbValueKeyStatus::Revoked,
};
DbValueKeyInternal::V1 {
id,
usage,
status,
status_cid,
der: der.clone(),
valid_from: *valid_from,
}
},
)
.collect()
}
}
impl ValueSetT for ValueSetKeyInternal {
fn insert_checked(&mut self, value: crate::value::Value) -> Result<bool, OperationError> {
match value {
// I'm not sure we ever need to actually push this?
/*
Value::KeyInternal {
id,
usage,
valid_from,
status,
der,
} => {
todo!();
}
*/
_ => {
debug_assert!(false);
Err(OperationError::InvalidValueState)
}
}
}
fn clear(&mut self) {
// When is this called?
debug_assert!(false);
self.map.clear();
}
fn remove(&mut self, pv: &crate::value::PartialValue, _cid: &Cid) -> bool {
match pv {
PartialValue::HexString(kid) => {
if let Some(key_object) = self.map.get_mut(kid) {
if !matches!(key_object.status, KeyStatus::Revoked) {
// Do we need to track the Cid like sessions?
key_object.status = KeyStatus::Revoked;
true
} else {
false
}
} else {
false
}
}
_ => false,
}
}
fn purge(&mut self, cid: &Cid) -> bool {
for key_object in self.map.values_mut() {
if !matches!(key_object.status, KeyStatus::Revoked) {
key_object.status_cid = cid.clone();
key_object.status = KeyStatus::Revoked;
}
}
false
}
fn trim(&mut self, trim_cid: &Cid) {
self.map.retain(|_, key_internal| {
match &key_internal.status {
KeyStatus::Revoked if &key_internal.status_cid < trim_cid => {
// This value is past the replication trim window and can now safely
// be removed
false
}
// Retain all else
_ => true,
}
});
}
fn contains(&self, pv: &crate::value::PartialValue) -> bool {
match pv {
PartialValue::HexString(kid) => self.map.contains_key(kid),
_ => false,
}
}
fn substring(&self, _pv: &crate::value::PartialValue) -> bool {
false
}
fn startswith(&self, _pv: &PartialValue) -> bool {
false
}
fn endswith(&self, _pv: &PartialValue) -> bool {
false
}
fn lessthan(&self, _pv: &crate::value::PartialValue) -> bool {
false
}
fn len(&self) -> usize {
self.map.len()
}
fn generate_idx_eq_keys(&self) -> Vec<String> {
self.map.keys().map(|kid| hex::encode(kid)).collect()
}
fn syntax(&self) -> SyntaxType {
SyntaxType::KeyInternal
}
fn validate(&self, _schema_attr: &crate::schema::SchemaAttribute) -> bool {
// Validate that every key id is a valid iname.
self.map.keys().all(|s| {
// We validate these two first to prevent injection attacks.
Value::validate_str_escapes(s)
&& Value::validate_singleline(s)
&& Value::validate_hexstr(s.as_str())
})
}
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(self.map.iter().map(|(kid, key_object)| {
format!(
"{}: {} {} {}",
kid, key_object.status, key_object.usage, key_object.valid_from
)
}))
}
fn to_db_valueset_v2(&self) -> DbValueSetV2 {
let keys = self.to_vec_dbvs();
DbValueSetV2::KeyInternal(keys)
}
fn to_repl_v1(&self) -> ReplAttrV1 {
let set = self.to_vec_dbvs();
ReplAttrV1::KeyInternal { set }
}
fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = crate::value::PartialValue> + '_> {
Box::new(self.map.keys().cloned().map(PartialValue::HexString))
}
fn to_value_iter(&self) -> Box<dyn Iterator<Item = crate::value::Value> + '_> {
debug_assert!(false);
Box::new(self.map.iter().map(
|(
id,
KeyInternalData {
usage,
status,
status_cid,
der,
valid_from,
},
)| {
Value::KeyInternal {
id: id.clone(),
usage: usage.clone(),
status: status.clone(),
status_cid: status_cid.clone(),
der: der.clone(),
valid_from: *valid_from,
}
},
))
}
fn equal(&self, other: &super::ValueSet) -> bool {
if let Some(other) = other.as_key_internal_map() {
&self.map == other
} else {
debug_assert!(false);
false
}
}
fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
let Some(b) = other.as_key_internal_map() else {
debug_assert!(false);
return Err(OperationError::InvalidValueState);
};
for (k_other, v_other) in b.iter() {
if let Some(v_self) = self.map.get_mut(k_other) {
// Revoked is always a greater status than retained or valid.
if v_other.status > v_self.status {
*v_self = v_other.clone();
}
} else {
// Not present, just insert.
self.map.insert(k_other.clone(), v_other.clone());
}
}
Ok(())
}
fn as_key_internal_map(&self) -> Option<&BTreeMap<KeyId, KeyInternalData>> {
Some(&self.map)
}
fn repl_merge_valueset(&self, older: &ValueSet, trim_cid: &Cid) -> Option<ValueSet> {
let Some(b) = older.as_key_internal_map() else {
return None;
};
let mut map = self.map.clone();
for (k_other, v_other) in b.iter() {
if let Some(v_self) = map.get_mut(k_other) {
// Revoked is always a greater status than retained or valid.
if v_other.status > v_self.status {
*v_self = v_other.clone();
}
} else {
// Not present, just insert.
map.insert(k_other.clone(), v_other.clone());
}
}
let mut vs = Box::new(ValueSetKeyInternal { map });
vs.trim(trim_cid);
Some(vs)
}
}
#[cfg(test)]
mod tests {
use super::{KeyInternalData, ValueSetKeyInternal};
use crate::prelude::*;
use crate::value::*;
#[test]
fn test_valueset_key_internal_purge_trim() {
let kid = "test".to_string();
let usage = KeyUsage::JwsEs256;
let valid_from = 0;
let status = KeyStatus::Valid;
let status_cid = Cid::new_zero();
let der = Vec::default();
let mut vs_a: ValueSet =
ValueSetKeyInternal::new(kid.clone(), usage, valid_from, status, status_cid, der);
let one_cid = Cid::new_count(1);
// Simulate session revocation.
vs_a.purge(&one_cid);
assert!(vs_a.len() == 1);
let key_internal = vs_a
.as_key_internal_map()
.and_then(|map| map.get(&kid))
.expect("Unable to locate session");
assert_eq!(key_internal.status, KeyStatus::Revoked);
assert_eq!(key_internal.status_cid, one_cid);
// Now trim
let two_cid = Cid::new_count(2);
vs_a.trim(&two_cid);
assert!(vs_a.is_empty());
}
#[test]
fn test_valueset_key_internal_merge_left() {
let kid = "test".to_string();
let usage = KeyUsage::JwsEs256;
let valid_from = 0;
let status = KeyStatus::Valid;
let status_cid = Cid::new_zero();
let der = Vec::default();
let mut vs_a: ValueSet = ValueSetKeyInternal::new(
kid.clone(),
usage,
valid_from,
status,
status_cid.clone(),
der.clone(),
);
let status = KeyStatus::Revoked;
let vs_b: ValueSet =
ValueSetKeyInternal::new(kid.clone(), usage, valid_from, status, status_cid, der);
vs_a.merge(&vs_b).expect("Failed to merge");
assert!(vs_a.len() == 1);
let key_internal = vs_a
.as_key_internal_map()
.and_then(|map| map.get(&kid))
.expect("Unable to locate session");
assert_eq!(key_internal.status, KeyStatus::Revoked);
}
#[test]
fn test_valueset_key_internal_merge_right() {
let kid = "test".to_string();
let usage = KeyUsage::JwsEs256;
let valid_from = 0;
let status = KeyStatus::Valid;
let status_cid = Cid::new_zero();
let der = Vec::default();
let vs_a: ValueSet = ValueSetKeyInternal::new(
kid.clone(),
usage,
valid_from,
status,
status_cid.clone(),
der.clone(),
);
let status = KeyStatus::Revoked;
let mut vs_b: ValueSet =
ValueSetKeyInternal::new(kid.clone(), usage, valid_from, status, status_cid, der);
vs_b.merge(&vs_a).expect("Failed to merge");
assert!(vs_b.len() == 1);
let key_internal = vs_b
.as_key_internal_map()
.and_then(|map| map.get(&kid))
.expect("Unable to locate session");
assert_eq!(key_internal.status, KeyStatus::Revoked);
}
#[test]
fn test_valueset_key_internal_repl_merge_left() {
let kid = "test".to_string();
let usage = KeyUsage::JwsEs256;
let valid_from = 0;
let status = KeyStatus::Valid;
let zero_cid = Cid::new_zero();
let one_cid = Cid::new_count(1);
let two_cid = Cid::new_count(2);
let der = Vec::default();
let kid_2 = "key_2".to_string();
let vs_a: ValueSet = ValueSetKeyInternal::from_key_iter(
[
(
kid.clone(),
KeyInternalData {
usage,
valid_from,
status,
status_cid: two_cid.clone(),
der: der.clone(),
},
),
(
kid_2.clone(),
KeyInternalData {
usage,
valid_from,
status: KeyStatus::Revoked,
status_cid: zero_cid.clone(),
der: der.clone(),
},
),
]
.into_iter(),
)
.expect("Failed to build valueset");
let status = KeyStatus::Revoked;
let vs_b: ValueSet =
ValueSetKeyInternal::new(kid.clone(), usage, valid_from, status, two_cid, der);
let vs_r = vs_a
.repl_merge_valueset(&vs_b, &one_cid)
.expect("Failed to merge");
let key_internal_map = vs_r.as_key_internal_map().expect("Unable to access map");
eprintln!("{:?}", key_internal_map);
assert!(vs_r.len() == 1);
let key_internal = key_internal_map.get(&kid).expect("Unable to access key");
assert_eq!(key_internal.status, KeyStatus::Revoked);
// Assert the item was trimmed
assert!(!key_internal_map.contains_key(&kid_2));
}
#[test]
fn test_valueset_key_internal_repl_merge_right() {
let kid = "test".to_string();
let usage = KeyUsage::JwsEs256;
let valid_from = 0;
let status = KeyStatus::Valid;
let zero_cid = Cid::new_zero();
let one_cid = Cid::new_count(1);
let two_cid = Cid::new_count(2);
let der = Vec::default();
let kid_2 = "key_2".to_string();
let vs_a: ValueSet = ValueSetKeyInternal::from_key_iter(
[
(
kid.clone(),
KeyInternalData {
usage,
valid_from,
status,
status_cid: two_cid.clone(),
der: der.clone(),
},
),
(
kid_2.clone(),
KeyInternalData {
usage,
valid_from,
status: KeyStatus::Revoked,
status_cid: zero_cid.clone(),
der: der.clone(),
},
),
]
.into_iter(),
)
.expect("Failed to build valueset");
let status = KeyStatus::Revoked;
let vs_b: ValueSet =
ValueSetKeyInternal::new(kid.clone(), usage, valid_from, status, two_cid, der);
let vs_r = vs_b
.repl_merge_valueset(&vs_a, &one_cid)
.expect("Failed to merge");
let key_internal_map = vs_r.as_key_internal_map().expect("Unable to access map");
eprintln!("{:?}", key_internal_map);
assert!(vs_r.len() == 1);
let key_internal = key_internal_map.get(&kid).expect("Unable to access key");
assert_eq!(key_internal.status, KeyStatus::Revoked);
// Assert the item was trimmed
assert!(!key_internal_map.contains_key(&kid_2));
}
}

View file

@ -21,6 +21,7 @@ use crate::credential::{totp::Totp, Credential};
use crate::prelude::*;
use crate::repl::{cid::Cid, proto::ReplAttrV1};
use crate::schema::SchemaAttribute;
use crate::server::keys::KeyId;
use crate::value::{Address, ApiToken, CredentialType, IntentTokenState, Oauth2Session, Session};
pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
@ -34,12 +35,14 @@ pub use self::cred::{
};
pub use self::datetime::ValueSetDateTime;
pub use self::eckey::ValueSetEcKeyPrivate;
pub use self::hexstring::ValueSetHexString;
use self::image::ValueSetImage;
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::key_internal::{KeyInternalData, ValueSetKeyInternal};
pub use self::nsuniqueid::ValueSetNsUniqueId;
pub use self::oauth::{
OauthClaimMapping, ValueSetOauthClaimMap, ValueSetOauthScope, ValueSetOauthScopeMap,
@ -65,12 +68,14 @@ mod cid;
mod cred;
mod datetime;
pub mod eckey;
mod hexstring;
pub mod image;
mod iname;
mod index;
mod iutf8;
mod json;
mod jws;
mod key_internal;
mod nsuniqueid;
mod oauth;
mod restricted;
@ -370,6 +375,16 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
None
}
fn as_key_internal_map(&self) -> Option<&BTreeMap<KeyId, KeyInternalData>> {
debug_assert!(false);
None
}
fn as_hexstring_set(&self) -> Option<&BTreeSet<String>> {
debug_assert!(false);
None
}
fn to_value_single(&self) -> Option<Value> {
if self.len() != 1 {
None
@ -684,7 +699,9 @@ pub fn from_result_value_iter(
| Value::OauthClaimMap(_, _)
| Value::OauthClaimValue(_, _, _)
| Value::JwsKeyEs256(_)
| Value::JwsKeyRs256(_) => {
| Value::JwsKeyRs256(_)
| Value::HexString(_)
| Value::KeyInternal { .. } => {
debug_assert!(false);
return Err(OperationError::InvalidValueState);
}
@ -751,6 +768,17 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
Value::OauthClaimValue(name, group, claims) => {
ValueSetOauthClaimMap::new_value(name, group, claims)
}
Value::HexString(s) => ValueSetHexString::new(s),
Value::KeyInternal {
id,
usage,
valid_from,
status,
status_cid,
der,
} => ValueSetKeyInternal::new(id, usage, valid_from, status, status_cid, der),
Value::PhoneNumber(_, _) => {
debug_assert!(false);
return Err(OperationError::InvalidValueState);
@ -812,6 +840,8 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
ValueSetWebauthnAttestationCaList::from_dbvs2(ca_list)
}
DbValueSetV2::OauthClaimMap(set) => ValueSetOauthClaimMap::from_dbvs2(set),
DbValueSetV2::KeyInternal(set) => ValueSetKeyInternal::from_dbvs2(set),
DbValueSetV2::HexString(set) => ValueSetHexString::from_dbvs2(set),
}
}
@ -862,5 +892,7 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
ValueSetWebauthnAttestationCaList::from_repl_v1(ca_list)
}
ReplAttrV1::OauthClaimMap { set } => ValueSetOauthClaimMap::from_repl_v1(set),
ReplAttrV1::KeyInternal { set } => ValueSetKeyInternal::from_repl_v1(set),
ReplAttrV1::HexString { set } => ValueSetHexString::from_repl_v1(set),
}
}

View file

@ -411,6 +411,10 @@ impl ValueSetT for ValueSetSession {
}
fn trim(&mut self, trim_cid: &Cid) {
// There might be a neater way to do this with less iterations. The problem
// is we can't just check on what was in b/older, because then we miss
// trimmable content from the local map. So once the merge is complete we
// do a pass for trim.
self.map.retain(|_, session| {
match &session.state {
SessionState::RevokedAt(cid) if cid < trim_cid => {
@ -660,44 +664,33 @@ impl ValueSetT for ValueSetSession {
}
fn repl_merge_valueset(&self, older: &ValueSet, trim_cid: &Cid) -> Option<ValueSet> {
if let Some(b) = older.as_session_map() {
// We can't just do merge maps here, we have to be aware of the
// session.state value and what it currently is set to.
let mut map = self.map.clone();
for (k_other, v_other) in b.iter() {
if let Some(v_self) = map.get_mut(k_other) {
// We only update if lower. This is where RevokedAt
// always proceeds other states, and lower revoked
// cids will always take effect.
if v_other.state > v_self.state {
*v_self = v_other.clone();
}
} else {
// Not present, just insert.
map.insert(*k_other, v_other.clone());
// If the older value has a different type - return nothing, we
// just take the newer value.
let Some(b) = older.as_session_map() else {
return None;
};
// We can't just do merge maps here, we have to be aware of the
// session.state value and what it currently is set to.
let mut map = self.map.clone();
for (k_other, v_other) in b.iter() {
if let Some(v_self) = map.get_mut(k_other) {
// We only update if lower. This is where RevokedAt
// always proceeds other states, and lower revoked
// cids will always take effect.
if v_other.state > v_self.state {
*v_self = v_other.clone();
}
} else {
// Not present, just insert.
map.insert(*k_other, v_other.clone());
}
// There might be a neater way to do this with less iterations. The problem
// is we can't just check on what was in b/older, because then we miss
// trimmable content from the local map. So once the merge is complete we
// do a pass for trim.
map.retain(|_, session| {
match &session.state {
SessionState::RevokedAt(cid) if cid < trim_cid => {
// This value is past the replication trim window and can now safely
// be removed
false
}
// Retain all else
_ => true,
}
});
Some(Box::new(ValueSetSession { map }))
} else {
// The older value has a different type - return nothing, we
// just take the newer value.
None
}
let mut vs = Box::new(ValueSetSession { map });
vs.trim(trim_cid);
Some(vs)
}
}
@ -1067,6 +1060,10 @@ impl ValueSetT for ValueSetOauth2Session {
}
fn trim(&mut self, trim_cid: &Cid) {
// There might be a neater way to do this with less iterations. The problem
// is we can't just check on what was in b/older, because then we miss
// trimmable content from the local map. So once the merge is complete we
// do a pass for trim.
self.map.retain(|_, session| {
match &session.state {
SessionState::RevokedAt(cid) if cid < trim_cid => {
@ -1292,22 +1289,12 @@ impl ValueSetT for ValueSetOauth2Session {
map.insert(*k_other, v_other.clone());
}
}
// There might be a neater way to do this with less iterations. The problem
// is we can't just check on what was in b/older, because then we miss
// trimmable content from the local map. So once the merge is complete we
// do a pass for trim.
map.retain(|_, session| {
match &session.state {
SessionState::RevokedAt(cid) if cid < trim_cid => {
// This value is past the replication trim window and can now safely
// be removed
false
}
// Retain all else
_ => true,
}
});
Some(Box::new(ValueSetOauth2Session { map, rs_filter }))
let mut vs = Box::new(ValueSetOauth2Session { map, rs_filter });
vs.trim(trim_cid);
Some(vs)
} else {
// The older value has a different type - return nothing, we
// just take the newer value.

View file

@ -1,13 +1,11 @@
//! Integration tests using browser automation
// use std::process::Output;
// use tempfile::tempdir;
use compact_jwt::{traits::JwsVerifiable, JwsCompact};
use kanidm_client::KanidmClient;
use kanidmd_lib::constants::EntryClass;
use kanidmd_testkit::login_put_admin_idm_admins;
// use testkit_macros::cli_kanidm;
use std::str::FromStr;
/// Tries to handle closing the webdriver session if there's an error
#[allow(unused_macros)]
@ -210,7 +208,14 @@ async fn test_webdriver_user_login(rsclient: kanidm_client::KanidmClient) {
#[kanidmd_testkit::test]
async fn test_domain_reset_token_key(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
assert!(rsclient.idm_domain_reset_token_key().await.is_ok());
let token = rsclient.get_token().await.expect("No bearer token present");
let jwt = JwsCompact::from_str(&token).expect("Failed to parse jwt");
let key_id = jwt.kid().expect("token does not have a key id");
assert!(rsclient.idm_domain_revoke_key(&key_id).await.is_ok());
}
#[kanidmd_testkit::test]

View file

@ -18,7 +18,7 @@ use tracing::{debug, trace};
use std::str::FromStr;
use compact_jwt::{JwsCompact, JwsEs256Verifier, JwsVerifier};
use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
use webauthn_authenticator_rs::softpasskey::SoftPasskey;
use webauthn_authenticator_rs::WebauthnAuthenticator;
@ -1402,12 +1402,17 @@ async fn test_server_api_token_lifecycle(rsclient: KanidmClient) {
// Decode it?
let token_unverified = JwsCompact::from_str(&token).expect("Failed to parse apitoken");
let jws_verifier = JwsEs256Verifier::try_from(
token_unverified
.get_jwk_pubkey()
.expect("No pubkey in token"),
)
.expect("Unable to build verifier");
let key_id = token_unverified
.kid()
.expect("token does not have a key id");
assert!(token_unverified.get_jwk_pubkey().is_none());
let jwk = rsclient
.get_public_jwk(key_id)
.await
.expect("Unable to get jwk");
let jws_verifier = JwsEs256Verifier::try_from(&jwk).expect("Unable to build verifier");
let token = jws_verifier
.verify(&token_unverified)
@ -1604,9 +1609,15 @@ async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) {
let jwt = JwsCompact::from_str(&token).expect("Failed to parse jwt");
let jws_verifier =
JwsEs256Verifier::try_from(jwt.get_jwk_pubkey().expect("No pubkey in token"))
.expect("Unable to build verifier");
let key_id = jwt.kid().expect("token does not have a key id");
assert!(jwt.get_jwk_pubkey().is_none());
let jwk = rsclient
.get_public_jwk(key_id)
.await
.expect("Unable to get jwk");
let jws_verifier = JwsEs256Verifier::try_from(&jwk).expect("Unable to build verifier");
let token: UserAuthToken = jws_verifier
.verify(&jwt)
@ -1678,9 +1689,15 @@ async fn test_server_user_auth_reauthentication(rsclient: KanidmClient) {
let jwt = JwsCompact::from_str(&token).expect("Failed to parse jwt");
let jws_verifier =
JwsEs256Verifier::try_from(jwt.get_jwk_pubkey().expect("No pubkey in token"))
.expect("Unable to build verifier");
let key_id = jwt.kid().expect("token does not have a key id");
assert!(jwt.get_jwk_pubkey().is_none());
let jwk = rsclient
.get_public_jwk(key_id)
.await
.expect("Unable to get jwk");
let jws_verifier = JwsEs256Verifier::try_from(&jwk).expect("Unable to build verifier");
let uat: UserAuthToken = jws_verifier
.verify(&jwt)
@ -1718,9 +1735,9 @@ async fn test_server_user_auth_reauthentication(rsclient: KanidmClient) {
let jwt = JwsCompact::from_str(&token).expect("Failed to parse jwt");
let jws_verifier =
JwsEs256Verifier::try_from(jwt.get_jwk_pubkey().expect("No pubkey in token"))
.expect("Unable to build verifier");
let key_id_2 = jwt.kid().expect("token does not have a key id");
assert_eq!(key_id, key_id_2);
assert!(jwt.get_jwk_pubkey().is_none());
let uat: UserAuthToken = jws_verifier
.verify(&jwt)
@ -1807,9 +1824,15 @@ async fn start_password_session(
let jwt = JwsCompact::from_str(&jwt).expect("Failed to parse jwt");
let jws_verifier =
JwsEs256Verifier::try_from(jwt.get_jwk_pubkey().expect("No pubkey in token"))
.expect("Unable to build verifier");
let key_id = jwt.kid().expect("token does not have a key id");
assert!(jwt.get_jwk_pubkey().is_none());
let jwk = rsclient
.get_public_jwk(key_id)
.await
.expect("Unable to get jwk");
let jws_verifier = JwsEs256Verifier::try_from(&jwk).expect("Unable to build verifier");
let uat: UserAuthToken = jws_verifier
.verify(&jwt)

View file

@ -1,4 +1,4 @@
use compact_jwt::{JwsCompact, JwsEs256Verifier, JwsVerifier};
use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier};
use kanidm_client::KanidmClient;
use kanidm_proto::internal::ScimSyncToken;
use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
@ -29,7 +29,6 @@ async fn test_sync_account_lifecycle(rsclient: KanidmClient) {
.await
.unwrap();
println!("{:?}", a);
let sync_entry = a.expect("No sync account was created?!");
// Shouldn't have a cred portal.
@ -77,12 +76,17 @@ async fn test_sync_account_lifecycle(rsclient: KanidmClient) {
let token_unverified = JwsCompact::from_str(&token).expect("Failed to parse apitoken");
let jws_verifier = JwsEs256Verifier::try_from(
token_unverified
.get_jwk_pubkey()
.expect("No pubkey in token"),
)
.expect("Unable to build verifier");
let key_id = token_unverified
.kid()
.expect("token does not have a key id");
assert!(token_unverified.get_jwk_pubkey().is_none());
let jwk = rsclient
.get_public_jwk(key_id)
.await
.expect("Unable to get jwk");
let jws_verifier = JwsEs256Verifier::try_from(&jwk).expect("Unable to build verifier");
let token = jws_verifier
.verify(&token_unverified)

View file

@ -1,8 +1,7 @@
use std::env;
use std::str::FromStr;
use async_recursion::async_recursion;
use compact_jwt::{JwsCompact, JwsEs256Verifier, JwsVerifier, JwtError};
use compact_jwt::{traits::JwsVerifiable, JwsCompact, JwsEs256Verifier, JwsVerifier, JwtError};
use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, Select};
use kanidm_client::{KanidmClient, KanidmClientBuilder};
@ -31,8 +30,10 @@ impl CommonOpt {
pub fn to_unauth_client(&self) -> KanidmClient {
let config_path: String = shellexpand::tilde(DEFAULT_CLIENT_CONFIG_PATH_HOME).into_owned();
let instance_name: Option<&str> = self.instance.as_deref();
let client_builder = KanidmClientBuilder::new()
.read_options_from_optional_config(DEFAULT_CLIENT_CONFIG_PATH)
.read_options_from_optional_instance_config(DEFAULT_CLIENT_CONFIG_PATH, instance_name)
.map_err(|e| {
error!(
"Failed to parse config ({:?}) -- {:?}",
@ -41,7 +42,7 @@ impl CommonOpt {
e
})
.and_then(|cb| {
cb.read_options_from_optional_config(&config_path)
cb.read_options_from_optional_instance_config(&config_path, instance_name)
.map_err(|e| {
error!("Failed to parse config ({:?}) -- {:?}", config_path, e);
e
@ -99,8 +100,9 @@ impl CommonOpt {
async fn try_to_client(&self, optype: OpType) -> Result<KanidmClient, ToClientError> {
let client = self.to_unauth_client();
// Read the token file.
let tokens = match read_tokens(&client.get_token_cache_path()) {
let token_store = match read_tokens(&client.get_token_cache_path()) {
Ok(t) => t,
Err(_e) => {
error!("Error retrieving authentication token store");
@ -108,19 +110,20 @@ impl CommonOpt {
}
};
if tokens.is_empty() {
let Some(token_instance) = token_store.instances(&self.instance) else {
error!(
"No valid authentication tokens found. Please login with the 'login' subcommand."
);
return Err(ToClientError::Other);
}
};
// If we have a username, use that to select tokens
let (spn, token) = match &self.username {
let (spn, jwsc) = match &self.username {
Some(filter_username) => {
let possible_token = if filter_username.contains('@') {
// If there is an @, it's an spn so just get the token directly.
tokens
token_instance
.tokens()
.get(filter_username)
.map(|t| (filter_username.clone(), t.clone()))
} else {
@ -135,7 +138,8 @@ impl CommonOpt {
filter_username_with_hostname
);
let mut token_refs: Vec<_> = tokens
let mut token_refs: Vec<_> = token_instance
.tokens()
.iter()
.filter(|(t, _)| *t == &filter_username_with_hostname)
.map(|(k, v)| (k.clone(), v.clone()))
@ -148,9 +152,11 @@ impl CommonOpt {
// otherwise let's try the fallback
let filter_username = format!("{}@", filter_username);
// Filter for tokens that match the pattern
let mut token_refs: Vec<_> = tokens
.into_iter()
let mut token_refs: Vec<_> = token_instance
.tokens()
.iter()
.filter(|(t, _)| t.starts_with(&filter_username))
.map(|(s, j)| (s.clone(), j.clone()))
.collect();
match token_refs.len() {
@ -177,16 +183,23 @@ impl CommonOpt {
}
}
None => {
if tokens.len() == 1 {
if token_instance.tokens().len() == 1 {
#[allow(clippy::expect_used)]
let (f_uname, f_token) = tokens.iter().next().expect("Memory Corruption");
let (f_uname, f_token) = token_instance
.tokens()
.iter()
.next()
.expect("Memory Corruption");
// else pick the first token
debug!("Using cached token for name {}", f_uname);
(f_uname.clone(), f_token.clone())
} else {
// Unable to automatically select the user because multiple tokens exist
// so we'll prompt the user to select one
match prompt_for_username_get_values(&client.get_token_cache_path()) {
match prompt_for_username_get_values(
&client.get_token_cache_path(),
&self.instance,
) {
Ok(tuple) => tuple,
Err(msg) => {
error!("Error: {}", msg);
@ -197,26 +210,23 @@ impl CommonOpt {
}
};
let jwsc = match JwsCompact::from_str(&token) {
Ok(jwtu) => jwtu,
Err(e) => {
error!("Unable to parse token - {:?}", e);
return Err(ToClientError::Other);
}
let Some(key_id) = jwsc.kid() else {
error!("token invalid, not key id associated");
return Err(ToClientError::Other);
};
let Some(pub_jwk) = token_instance.keys().get(key_id) else {
error!("token invalid, no cached jwk available");
return Err(ToClientError::Other);
};
// Is the token (probably) valid?
let jws_verifier = if let Some(pub_jwk) = jwsc.get_jwk_pubkey() {
match JwsEs256Verifier::try_from(pub_jwk) {
Ok(verifier) => verifier,
Err(err) => {
error!(?err, "Unable to configure jws verifier");
return Err(ToClientError::Other);
}
let jws_verifier = match JwsEs256Verifier::try_from(pub_jwk) {
Ok(verifier) => verifier,
Err(err) => {
error!(?err, "Unable to configure jws verifier");
return Err(ToClientError::Other);
}
} else {
error!("Unable to access token public key");
return Err(ToClientError::Other);
};
match jws_verifier.verify(&jwsc).and_then(|jws| {
@ -259,7 +269,7 @@ impl CommonOpt {
};
// Set it into the client
client.set_token(token).await;
client.set_token(jwsc.to_string()).await;
Ok(client)
}
@ -323,17 +333,26 @@ impl CommonOpt {
/// This parses the token store and prompts the user to select their username, returns the username/token as a tuple of Strings
///
/// Used to reduce duplication in implementing [prompt_for_username_get_username] and `prompt_for_username_get_token`
pub fn prompt_for_username_get_values(token_cache_path: &str) -> Result<(String, String), String> {
let tokens = match read_tokens(token_cache_path) {
pub fn prompt_for_username_get_values(
token_cache_path: &str,
instance_name: &Option<String>,
) -> Result<(String, JwsCompact), String> {
let token_store = match read_tokens(token_cache_path) {
Ok(value) => value,
_ => return Err("Error retrieving authentication token store".to_string()),
};
if tokens.is_empty() {
let Some(token_instance) = token_store.instances(instance_name) else {
error!("No tokens in store, quitting!");
std::process::exit(1);
};
if token_instance.tokens().is_empty() {
error!("No tokens in store, quitting!");
std::process::exit(1);
}
let mut options = Vec::new();
for option in tokens.iter() {
for option in token_instance.tokens().iter() {
options.push(String::from(option.0));
}
let user_select = Select::with_theme(&ColorfulTheme::default())
@ -350,12 +369,12 @@ pub fn prompt_for_username_get_values(token_cache_path: &str) -> Result<(String,
};
debug!("Index of the chosen menu item: {:?}", selection);
match tokens.iter().nth(selection) {
match token_instance.tokens().iter().nth(selection) {
Some(value) => {
let (f_uname, f_token) = value;
debug!("Using cached token for name {}", f_uname);
debug!("Cached token: {}", f_token);
Ok((f_uname.to_string(), f_token.to_string()))
Ok((f_uname.to_string(), f_token.clone()))
}
None => {
error!("Memory corruption trying to read token store, quitting!");
@ -367,8 +386,11 @@ pub fn prompt_for_username_get_values(token_cache_path: &str) -> Result<(String,
/// This parses the token store and prompts the user to select their username, returns the username as a String
///
/// Powered by [prompt_for_username_get_values]
pub fn prompt_for_username_get_username(token_cache_path: &str) -> Result<String, String> {
match prompt_for_username_get_values(token_cache_path) {
pub fn prompt_for_username_get_username(
token_cache_path: &str,
instance_name: &Option<String>,
) -> Result<String, String> {
match prompt_for_username_get_values(token_cache_path, instance_name) {
Ok(value) => {
let (f_user, _) = value;
Ok(f_user)

View file

@ -7,8 +7,8 @@ impl DomainOpt {
DomainOpt::SetDisplayName(copt) => copt.copt.debug,
DomainOpt::SetLdapBasedn { copt, .. }
| DomainOpt::SetLdapAllowUnixPasswordBind { copt, .. }
| DomainOpt::Show(copt)
| DomainOpt::ResetTokenKey(copt) => copt.debug,
| DomainOpt::RevokeKey { copt, .. }
| DomainOpt::Show(copt) => copt.debug,
}
}
@ -53,9 +53,9 @@ impl DomainOpt {
Err(e) => handle_client_error(e, copt.output_mode),
}
}
DomainOpt::ResetTokenKey(copt) => {
DomainOpt::RevokeKey { copt, key_id } => {
let client = copt.to_client(OpType::Write).await;
match client.idm_domain_reset_token_key().await {
match client.idm_domain_revoke_key(key_id).await {
Ok(_) => println!("Success"),
Err(e) => handle_client_error(e, copt.output_mode),
}

View file

@ -6,7 +6,9 @@ use std::io::{self, BufReader, BufWriter, ErrorKind, IsTerminal, Write};
use std::path::PathBuf;
use std::str::FromStr;
use compact_jwt::{JwsCompact, JwsEs256Verifier, JwsVerifier, JwtError};
use compact_jwt::{
traits::JwsVerifiable, Jwk, JwsCompact, JwsEs256Verifier, JwsVerifier, JwtError,
};
use dialoguer::theme::ColorfulTheme;
use dialoguer::Select;
use kanidm_client::{ClientError, KanidmClient};
@ -21,8 +23,96 @@ use crate::common::prompt_for_username_get_username;
use crate::webauthn::get_authenticator;
use crate::{CommonOpt, LoginOpt, LogoutOpt, ReauthOpt, SessionOpt};
use serde::{Deserialize, Serialize};
static TOKEN_DIR: &str = "~/.cache";
#[derive(Debug, Serialize, Clone, Deserialize, Default)]
pub struct TokenInstance {
keys: BTreeMap<String, Jwk>,
tokens: BTreeMap<String, JwsCompact>,
}
impl TokenInstance {
pub fn tokens(&self) -> &BTreeMap<String, JwsCompact> {
&self.tokens
}
pub fn keys(&self) -> &BTreeMap<String, Jwk> {
&self.keys
}
pub fn valid_uats(&self) -> BTreeMap<String, UserAuthToken> {
self.tokens
.iter()
.filter_map(|(u, jwsc)| {
// Ignore if it has no key id.
let key_id = jwsc.kid()?;
// Ignore if we can't verify
let pub_jwk = self.keys.get(key_id)?;
let jws_verifier = JwsEs256Verifier::try_from(pub_jwk)
.map_err(|e| {
error!(?e, "Unable to configure jws verifier");
})
.ok()?;
jws_verifier
.verify(jwsc)
.and_then(|jws| {
jws.from_json::<UserAuthToken>().map_err(|serde_err| {
error!(?serde_err);
JwtError::InvalidJwt
})
})
.map_err(|e| {
error!(?e, "Unable to verify token signature, may be corrupt");
})
.map(|uat| (u.clone(), uat))
.ok()
})
.collect()
}
pub fn cleanup(&mut self, now: time::OffsetDateTime) -> usize {
// It's not optimal to do this in this way, but we can't double borrow.
let retain = self.valid_uats();
let start_len = self.tokens.len();
self.tokens.retain(|spn, _tonk| {
if let Some(uat) = retain.get(spn) {
if let Some(exp) = uat.expiry {
// Retain if expiry is in future aka greater than now
exp > now
} else {
true
}
} else {
false
}
});
start_len - self.tokens.len()
}
}
#[derive(Debug, Serialize, Clone, Deserialize, Default)]
pub struct TokenStore {
instances: BTreeMap<Option<String>, TokenInstance>,
}
impl TokenStore {
pub fn instances(&self, name: &Option<String>) -> Option<&TokenInstance> {
self.instances.get(name)
}
pub fn instances_mut(&mut self, name: &Option<String>) -> Option<&mut TokenInstance> {
self.instances.get_mut(name)
}
}
impl CommonOpt {
fn get_token_cache_path(&self) -> String {
match self.token_cache_path.clone() {
@ -33,14 +123,14 @@ impl CommonOpt {
}
#[allow(clippy::result_unit_err)]
pub fn read_tokens(token_path: &str) -> Result<BTreeMap<String, String>, ()> {
pub fn read_tokens(token_path: &str) -> Result<TokenStore, ()> {
let token_path = PathBuf::from(shellexpand::tilde(token_path).into_owned());
if !token_path.exists() {
debug!(
"Token cache file path {:?} does not exist, returning an empty token store.",
token_path
);
return Ok(BTreeMap::new());
return Ok(Default::default());
}
debug!("Attempting to read tokens from {:?}", &token_path);
@ -64,7 +154,7 @@ pub fn read_tokens(token_path: &str) -> Result<BTreeMap<String, String>, ()> {
token_path.display(),
e
);
return Ok(BTreeMap::new());
return Ok(Default::default());
}
};
}
@ -73,7 +163,7 @@ pub fn read_tokens(token_path: &str) -> Result<BTreeMap<String, String>, ()> {
// Else try to read
serde_json::from_reader(reader).map_err(|e| {
error!(
warn!(
"JSON/IO error reading tokens from {:?} -> {:?}",
&token_path, e
);
@ -81,7 +171,7 @@ pub fn read_tokens(token_path: &str) -> Result<BTreeMap<String, String>, ()> {
}
#[allow(clippy::result_unit_err)]
pub fn write_tokens(tokens: &BTreeMap<String, String>, token_path: &str) -> Result<(), ()> {
pub fn write_tokens(tokens: &TokenStore, token_path: &str) -> Result<(), ()> {
let token_dir = PathBuf::from(shellexpand::tilde(TOKEN_DIR).into_owned());
let token_path = PathBuf::from(shellexpand::tilde(token_path).into_owned());
@ -248,6 +338,7 @@ async fn process_auth_state(
mut allowed: Vec<AuthAllowed>,
mut client: KanidmClient,
maybe_password: &Option<String>,
instance_name: &Option<String>,
) {
loop {
debug!("Allowed mechanisms -> {:?}", allowed);
@ -314,11 +405,11 @@ async fn process_auth_state(
// Loop again.
}
// Read the current tokens
let mut tokens = read_tokens(&client.get_token_cache_path()).unwrap_or_else(|_| {
error!("Error retrieving authentication token store");
std::process::exit(1);
});
// Read the current tokens. If we can't read them, IGNORE!!!
let mut tokens = read_tokens(&client.get_token_cache_path()).unwrap_or_default();
// Select our token instance. Create it if empty.
let token_instance = tokens.instances.entry(instance_name.clone()).or_default();
// Add our new one
let (spn, tonk) = match client.get_token().await {
@ -331,17 +422,35 @@ async fn process_auth_state(
}
};
let jws_verifier = if let Some(pub_jwk) = jwsc.get_jwk_pubkey() {
match JwsEs256Verifier::try_from(pub_jwk) {
Ok(verifier) => verifier,
let Some(key_id) = jwsc.kid() else {
error!("JWS invalid, not key id associated");
std::process::exit(1);
};
// Okay, lets check the jwk now.
let pub_jwk = if let Some(pub_jwk) = token_instance.keys.get(key_id).cloned() {
pub_jwk
} else {
// Get it from the server.
let pub_jwk = match client.get_public_jwk(&key_id).await {
Ok(pj) => pj,
Err(err) => {
error!(?err, "Unable to configure jws verifier");
error!(?err, "Unable to retrieve jwk from server");
std::process::exit(1);
}
};
token_instance
.keys
.insert(key_id.to_string(), pub_jwk.clone());
pub_jwk
};
let jws_verifier = match JwsEs256Verifier::try_from(&pub_jwk) {
Ok(verifier) => verifier,
Err(err) => {
error!(?err, "Unable to configure jws verifier");
std::process::exit(1);
}
} else {
error!("Unable to access token public key");
std::process::exit(1);
};
let tonk = match jws_verifier.verify(&jwsc).and_then(|jws| {
@ -358,8 +467,8 @@ async fn process_auth_state(
};
let spn = tonk.spn;
// Return the un-parsed token
(spn, t)
// Return the original jws
(spn, jwsc)
}
None => {
error!("Error retrieving client session");
@ -367,7 +476,7 @@ async fn process_auth_state(
}
};
tokens.insert(spn.clone(), tonk);
token_instance.tokens.insert(spn.clone(), tonk);
// write them out.
if write_tokens(&tokens, &client.get_token_cache_path()).is_err() {
@ -442,8 +551,10 @@ impl LoginOpt {
std::process::exit(1);
});
let instance_name = &self.copt.instance;
// We now have the first auth state, so we can proceed until complete.
process_auth_state(allowed, client, &self.password).await;
process_auth_state(allowed, client, &self.password, instance_name).await;
}
}
@ -455,12 +566,14 @@ impl ReauthOpt {
pub async fn exec(&self) {
let client = self.copt.to_client(OpType::Read).await;
let instance_name = &self.copt.instance;
let allowed = client.reauth_begin().await.unwrap_or_else(|e| {
error!("Error during reauthentication begin phase: {:?}", e);
std::process::exit(1);
});
process_auth_state(allowed, client, &None).await;
process_auth_state(allowed, client, &None, instance_name).await;
}
}
@ -470,6 +583,8 @@ impl LogoutOpt {
}
pub async fn exec(&self) {
let instance_name = &self.copt.instance;
let spn: String = if self.local_only {
// For now we just remove this from the token store.
let mut _tmp_username = String::new();
@ -478,7 +593,10 @@ impl LogoutOpt {
None => {
// check if we're in a tty
if std::io::stdin().is_terminal() {
match prompt_for_username_get_username(&self.copt.get_token_cache_path()) {
match prompt_for_username_get_username(
&self.copt.get_token_cache_path(),
instance_name,
) {
Ok(value) => value,
Err(msg) => {
error!("{}", msg);
@ -551,8 +669,13 @@ impl LogoutOpt {
std::process::exit(1);
});
let Some(token_instance) = tokens.instances.get_mut(instance_name) else {
println!("No sessions for {}", spn);
return;
};
// Remove our old one
if tokens.remove(&spn).is_some() {
if token_instance.tokens.remove(&spn).is_some() {
// write them out.
if let Err(_e) = write_tokens(&tokens, &self.copt.get_token_cache_path()) {
error!("Error persisting authentication token store");
@ -572,84 +695,48 @@ impl SessionOpt {
}
}
fn read_valid_tokens(token_cache_path: &str) -> BTreeMap<String, (String, UserAuthToken)> {
read_tokens(token_cache_path)
.unwrap_or_else(|_| {
error!("Error retrieving authentication token store");
std::process::exit(1);
})
.into_iter()
.filter_map(|(u, t)| {
let jwsc = JwsCompact::from_str(&t)
.map_err(|e| {
error!(?e, "Unable to parse token from str");
})
.ok()?;
let jws_verifier = jwsc.get_jwk_pubkey().and_then(|pub_jwk| {
JwsEs256Verifier::try_from(pub_jwk)
.map_err(|e| {
error!(?e, "Unable to configure jws verifier");
})
.ok()
})?;
jws_verifier
.verify(&jwsc)
.and_then(|jws| {
jws.from_json::<UserAuthToken>().map_err(|serde_err| {
error!(?serde_err);
JwtError::InvalidJwt
})
})
.map_err(|e| {
error!(?e, "Unable to verify token signature, may be corrupt");
})
.map(|uat| (u, (t, uat)))
.ok()
})
.collect()
}
pub async fn exec(&self) {
match self {
SessionOpt::List(copt) => {
let tokens = Self::read_valid_tokens(&copt.get_token_cache_path());
for (_, uat) in tokens.values() {
let token_store = read_tokens(&copt.get_token_cache_path()).unwrap_or_else(|_| {
error!("Error retrieving authentication token store");
std::process::exit(1);
});
let instance_name = &copt.instance;
let Some(token_instance) = token_store.instances(instance_name) else {
return;
};
for (_, uat) in token_instance.valid_uats() {
println!("---");
println!("{}", uat);
}
}
SessionOpt::Cleanup(copt) => {
let tokens = Self::read_valid_tokens(&copt.get_token_cache_path());
let start_len = tokens.len();
let mut token_store =
read_tokens(&copt.get_token_cache_path()).unwrap_or_else(|_| {
error!("Error retrieving authentication token store");
std::process::exit(1);
});
let instance_name = &copt.instance;
let Some(token_instance) = token_store.instances_mut(instance_name) else {
error!("No tokens for instance");
std::process::exit(1);
};
let now = time::OffsetDateTime::now_utc();
let change = token_instance.cleanup(now);
let tokens: BTreeMap<_, _> = tokens
.into_iter()
.filter_map(|(u, (t, uat))| {
if let Some(exp) = uat.expiry {
if now >= exp {
//Expired
None
} else {
Some((u, t))
}
} else {
Some((u, t))
}
})
.collect();
let end_len = tokens.len();
if let Err(_e) = write_tokens(&tokens, &copt.get_token_cache_path()) {
if let Err(_e) = write_tokens(&token_store, &copt.get_token_cache_path()) {
error!("Error persisting authentication token store");
std::process::exit(1);
};
println!("Removed {} sessions", start_len - end_len);
println!("Removed {} sessions", change);
}
}
}

View file

@ -57,6 +57,9 @@ pub struct CommonOpt {
/// Enable debugging of the kanidm tool
#[clap(short, long, env = "KANIDM_DEBUG")]
pub debug: bool,
/// Select the instance name you wish to connect to
#[clap(short='I', long="instance", env = "KANIDM_INSTANCE")]
pub instance: Option<String>,
/// The URL of the kanidm instance
#[clap(short = 'H', long = "url", env = "KANIDM_URL")]
pub addr: Option<String>,
@ -1147,10 +1150,14 @@ pub enum DomainOpt {
#[clap(name = "show")]
/// Show information about this system's domain
Show(CommonOpt),
#[clap(name = "reset-token-key")]
/// Reset this domain token signing key. This will cause all user sessions to be
#[clap(name = "revoke-key")]
/// Revoke a key by its key id. This will cause all user sessions to be
/// invalidated (logged out).
ResetTokenKey(CommonOpt),
RevokeKey {
#[clap(flatten)]
copt: CommonOpt,
key_id: String,
}
}
#[derive(Debug, Subcommand)]
@ -1403,3 +1410,4 @@ pub struct KanidmClientParser {
#[clap(subcommand)]
pub commands: KanidmClientOpt,
}